KEMBAR78
Support await'ing a Task without throwing · Issue #22144 · dotnet/runtime · GitHub
Skip to content

Support await'ing a Task without throwing #22144

@stephentoub

Description

@stephentoub

EDITED 5/24/2023 by @stephentoub:
Latest proposal at #22144 (comment)

namespace System.Threading.Tasks;

public class Task
{
+    public ConfiguredTaskAwaitable ConfigureAwait(ConfigureAwaitOptions options);
}

+[Flags]
+public enum ConfigureAwaitOptions
+{
+    None = 0,
+    ContinueOnCapturedContext = 0x1,
+    SuppressExceptions = 0x2,
+    ForceYielding = 0x4,
+    ForceAsynchronousContinuation = 0x8,
+}

(I'm not sure yet if we actually need to ship ForceAsynchronousContinuation now. We might hold off if we don't have direct need in our own uses.)


EDIT: See #22144 (comment) for an up-to-date API proposal.

namespace System.Threading.Tasks;

+[Flags]
+public enum TaskAwaitBehavior
+{
+    Default = 0x0,
+    NoContinueOnCapturedContext = 0x1,
+    NoThrow = 0x2,
+}

public partial class Task
{
     public ConfiguredTaskAwaitable ConfigureAwait(bool continueOnCapturedContext);
+    public ConfiguredTaskAwaitable ConfigureAwait(TaskAwaitBehavior awaitBehavior);
}

public partial class Task<TResult>
{
     public new ConfiguredTaskAwaitable<TResult> ConfigureAwait(bool continueOnCapturedContext);
+    public new ConfiguredTaskAwaitable<TResult> ConfigureAwait(TaskAwaitBehavior awaitBehavior);
}

public partial struct ValueTask
{
     public ConfiguredValueTaskAwaitable ConfigureAwait(bool continueOnCapturedContext);
+    public ConfiguredValueTaskAwaitable ConfigureAwait(TaskAwaitBehavior awaitBehavior);
}

public partial struct ValueTask<TResult>
{
     public ConfiguredTaskAwaitable<TResult> ConfigureAwait(bool continueOnCapturedContext);
+    public ConfiguredTaskAwaitable<TResult> ConfigureAwait(TaskAwaitBehavior awaitBehavior);
}
Original post Currently there isn't a great way to await a Task without throwing (if the task may have faulted or been canceled). You can simply eat all exceptions:
try { await task; } catch { }

but that incurs the cost of the throw and also triggers first-chance exception handling. You can use a continuation:

await task.ContinueWith(delegate { }, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);

but that incurs the cost of creating and running an extra task. The best way in terms of run-time overhead is to use a custom awaiter that has a nop GetResult:

internal struct NoThrowAwaiter : ICriticalNotifyCompletion
{
    private readonly Task _task;
    public NoThrowAwaiter(Task task) { _task = task; }
    public NoThrowAwaiter GetAwaiter() => this;
    public bool IsCompleted => _task.IsCompleted;
    public void GetResult() { }
    public void OnCompleted(Action continuation) => _task.GetAwaiter().OnCompleted(continuation);
    public void UnsafeOnCompleted(Action continuation) => OnCompleted(continuation);
}
...
await new NoThrowAwaiter(task);

but that's obviously more code than is desirable. It'd be nice if functionality similar to that last example was built-in.

Proposal
Add a new overload of ConfigureAwait, to both Task and Task<T>. Whereas the current overload accepts a bool, the new overload would accept a new ConfigureAwaitBehavior enum:

namespace System.Threading.Tasks
{
    [Flags]
    public enum ConfigureAwaitBehavior
    {
        NoCapturedContext = 0x1, // equivalent to ConfigureAwait(false)
        NoThrow = 0x2, // when set, no exceptions will be thrown for Faulted/Canceled
        Asynchronous = 0x4, // force the continuation to be asynchronous
        ... // other options we might want in the future
    }
}

Then with ConfigureAwait overloads:

namespace System.Threading.Tasks
{
    public class Task
    {
        ...
        public ConfiguredTaskAwaitable ConfigureAwait(ConfigureAwaitBehavior behavior);
    }

    public class Task<TResult> : Task
    {
        ...
        public ConfiguredTaskAwaitable<TResult> ConfigureAwait(ConfigureAwaitBehavior behavior);
    }
}

code that wants to await without throwing can write:

await task.ConfigureAwait(ConfigureAwaitBehavior.NoThrow);

or that wants to have the equivalent of ConfigureAwait(false) and also not throw:

await task.ConfigureAwait(ConfigureAwaitBehavior.NoCapturedContext | ConfigureAwaitBehavior.NoThrow);

etc.

From an implementation perspective, this will mean adding a small amount of logic to ConfiguredTaskAwaiter, so there's a small chance it could have a negative imp

Alternatives
An alternative would be to add a dedicated API like NoThrow to Task, either as an instance or as an extension method, e.g.

await task.NoThrow();

That however doesn't compose well with wanting to use ConfigureAwait(false), and we'd likely end up needing to add a full matrix of options and supporting awaitable/awaiter types to enable that.

Another option would be to add methods like NoThrow to ConfiguredTaskAwaitable, so you could write:

await task.ConfigureAwait(true).NoThrow();

etc.

And of course an alternative is to continue doing nothing and developers that need this can write their own awaiter like I did earlier.

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions