runtime: Support await'ing a Task without throwing

EDITED 5/24/2023 by @stephentoub: Latest proposal at https://github.com/dotnet/runtime/issues/22144#issuecomment-1561983918

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 https://github.com/dotnet/runtime/issues/22144#issuecomment-943445439 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.

About this issue

  • Original URL
  • State: closed
  • Created 7 years ago
  • Reactions: 33
  • Comments: 82 (79 by maintainers)

Most upvoted comments

Change ConfigureAwaitBehavior to AwaitBehavior?

task.ConfigureAwait(AwaitBehavior.NoCapturedContext | AwaitBehavior.NoThrow)

Please consider a shorter enum name that doesn’t start with a verb, e.g. what @willdean proposed (AwaitBehavior). ConfigureAwait(ConfigureAwaitBehavior.NoThrow) is a lot of redundancy.

A little bit of bike-shedding, but should the enum be AwaitBehavior rather than ConfigureAwaitBehavior?

After all, it’s describing the behavior of the await, not the behavior of the the ConfigureAwait function - the latter is just there to configure the behavior.

Video

We discussed this today, and one of the main feelings is that the ConfigureAwait(NoThrow) feels like a pit of failure. One piece of that is it invalidates the nullability annotations, since nulls will start appearing out of non-null-returning members.

Something like an extension method that changes from an awaitable<T> to an awaitable<(T?, Exception)>, or at least awaitable<(bool, T?)> might be reasonable, but that can’t be described as a flags flag. This is, in particular, important for the ValueTask versions, since the Exception cannot otherwise be recovered.

We probably want to try this out on non-public code first to get an idea of what’s really needed.

I keep coming across places where this would be valuable, and I’d like to get a solution into .NET 8. I think we’ve been making this too complicated…

We don’t need to support ValueTasks, which are problematic because you can’t look at them after you await them, and so any model for ValueTask would need to entail returning exception information, which is unfamiliar in the model; we can instead just say “if you need this and you have a ValueTask, call AsTask first”. Yes, it may allocate, but the use cases where you need this are more niche, you can check first whether it already completed, and AsTask for something that hasn’t yet completed will only allocate if it’s backed by an IValueTaskSource, which is itself currently problematic for such configuration because it doesn’t provide a way currently to get at the Exception without throwing it.

Further, we don’t need to support TResults. Previous concerns with TResult includes not knowing whether you can actually trust the returned TResult, and any nullability annotations possibly being wrong. But Task<TResult> derives from Task, so any support for Task “just works” for Task<TResult>, and in particular without violating anything about the TResult’s nature.

So, if we just expose this support for Task, we address the 99% case and we can make that work well.

Thus my proposal is simply this:

namespace System.Threading.Tasks;

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

+[Flags]
+public enum ConfigureAwaitOptions
+{
    /// <summary>No options specified.</summary>
    /// <remarks>
    /// This behaves identically to using <see cref="Task.ConfigureAwait(bool)"/> with a <see langword="false"/> argument.
    /// </remarks>
+   None = 0,

    /// <summary>
    /// Attempt to marshal the continuation back to the original <see cref="SynchronizationContext"/> or
    /// <see cref="TaskScheduler"/> present at the time of the await.
    /// </summary>
    /// <remarks>
    /// If there is no such context/scheduler, or if this option is not specified, the thread on
    /// which the continuation is invoked is unspecified and left up to the determination of the system.
    /// </remarks>
+   ContinueOnCapturedContext = 0x1,

    /// <summary>
    /// Avoids throwing an exception at the completion of an await on a <see cref="Task"/> that ends
    /// in the <see cref="TaskStatus.Faulted"/> or <see cref="TaskStatus.Canceled"/> state.
    /// </summary>
+   SuppressThrowing = 0x2,

    /// <summary>
    /// Forces an await on an already completed <see cref="Task"/> to behave as if the <see cref="Task"/>"/>
    /// wasn't yet completed, such that the current asynchronous method will be forced to yield its execution.
    /// </summary>
+   ForceYielding = 0x4,

    /// <summary>
    /// Forces any continuation created for this await to be queued asynchronously as part of the
    /// antecedent <see cref="Task"/>'s completion.
    /// </summary>
    /// <remarks>
    /// This flag is only relevant when awaiting a <see cref="Task"/> that hasn't yet completed,
    /// as it impacts how the continuation is stored in the <see cref="Task"/>.  When the <see cref="Task"/>
    /// eventually completes, if the <see cref="Task"/>'s completion would have otherwise synchronously
    /// invoked the continuation as part of its completion routine, this flag will instead force it
    /// to be queued for asynchronous execution.  This is a consumption-side equivalent to the
    /// <see cref="TaskCreationOptions.RunContinuationsAsynchronously"/> flag, which allows the producer
    /// of a <see cref="Task"/> to specify that all of that <see cref="Task"/>'s continuations must be
    /// queued rather than allowing any to be synchronously invoked.
    /// </remarks>
+   ForceAsynchronousContinuation = 0x8,
+}

Notes:

  • Whether we need a different return type for this ConfigureAwait overload will depend on implementation and perf testing. Hopefully we can get away with just using the same type as the one that exists. If we can’t, we’d add a new type with the same shape as ConfiguredTaskAwaitable but instead named ConfiguredWithOptionsTaskAwaitable (name can obviously be debated).
  • As was previously proposed by Pent, we could make this a new awaitable whose GetResult would return the original Task. That might help some cases, but I’m concerned it could be confusing (especially in a case where you have a Task<Task>).
  • This might be confusing for someone who actually cares about the current context, such as in client UI, and adds a .ConfigureAwait where they didn’t previously have one in order to suppress exceptions and neglects to also add ContinueOnCapturedContext. But to change that, we’d need to invert the name of ContinueOnCapturedContext.

Examples:

// Wait for a task to complete or a timeout, then see whether the task was successful
await task.WaitAsync(TimeSpan.FromSeconds(10)).ConfigureAwait(ConfigureAwaitOptions.SuppressExceptions);
if (task.IsCompleted) { ... } else { ... }

// Queue the remainder of the method to the thread pool
await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceAsynchronousCompletion);

I thought I’d revive this issue for 7.0.0. Given we’ve merged timeouts and cancellation via the WaitAsync approach in #47525, based on the prototype in 416cc3c07697082e921fd496e3b9871e92202946 I believe the following might be a viable API shape:

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);
}

Open Questions

  • Should NoThrow be supported in ValueTask’s?
  • Should NoThrow have impact on nullability annotations of the returned type? (would require authoring dedicated awaitable/awaiter types)

cc @stephentoub

I like the composable style of await task.ConfigureAwait(true).NoThrow()

Could we then have more clearly-named alternatives to ConfigureAwait(bool) which didn’t need a bool?

To someone who hasn’t been completely steeped in TPL, etc. from the outset I don’t think ConfigureAwait(false) is intuitive at all - does “false” mean it isn’t configured? Does it mean it isn’t awaiting? (Rhetorical, I don’t need an explanation myself)

I know a renamed ConfigureAwait alternative isn’t the subject of this issue, but could it go hand-in-hand with this change? What might a good name for ‘ConfigureAwait(false)’ be (assuming there’s any agreement that it could be improved)

cc @BrennanConroy

I’m planning to submit a pr to aspnetcore once it moves to a new enough runtime.

Personally I would be satisfied with receiving no information whatsoever after a non-throwing await. This covers 90% of my use cases.

I’m surprised by that. It’s rare in my experience to care that an operation has completed but to also not care whether it was successful, what result it produced if any, or what error it encountered if any. Such scenarios certainly exist, but in my experience they’re very far from the majority. I don’t believe we should ship an API for this that has such constraints.

I mean, strictly speaking it is pretending to not be completed so that the state machine is forced to pass its continuation. I’d probably be less concerned if this weren’t a public property.

This is how Task.Yield() is implemented:

https://github.com/dotnet/runtime/blob/8a52f1e948b6f22f418817ec1068f07b8dae2aa5/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/YieldAwaitable.cs#L50

would an exception still bubble up to TaskScheduler.UnobservedTaskException?

We can decide, but I think the answer should be “yes”. If someone doesn’t want that, they just check task.Exception, just as they can today.

if a developer writes var result = await task.ConfigureAwait(ConfigureAwaitBehavior.NoThrow) where task is Task<string>, what would result be in the case that task failed?

It would be default(T). With NoThrow, you have to check the status of task afterwards to know whether it was successful, faulted, or canceled, and you’d only use the result if it was valid.

A very similar issue for synchronous waiting: https://github.com/dotnet/corefx/issues/8142.

💭 I really wish there was a way to return a cancelled result from an async method without throwing the exception… related to dotnet/roslyn#19652.

Rather than NoCapturedContext, I would prefer to see:

ContinueOnCapturedContext,
ConfigureAwaitBehavior.Default = ContinueOnCapturedContext

Another helpful option would be Yield, which alters the behavior in cases where the antecedent is already complete in order to force a yield.

In the case 3 (enumerator.MoveNextAsync) I have included a detailed explanation about why the status of this task is irrelevant.

FWIW, I disagree with the assertions in that comment that the exception is irrelevant. First, an action the user’s code initiated failed with an arbitrary exception; from my perspective, it shouldn’t just be eaten by the implementation because the user happened to stop enumerating (and, yes, while exceptions are discouraged from Dispose, sometimes it’s the lesser evil). Second, the unobserved exception is then likely going to raise the TaskScheduler.UnobservedTaskException event, assuming the operation is actually backed by a task.

I realize this isn’t the main point of the discussion, nor do we need to debate the semantics of an API we’re not going to ship. I’m simply trying to highlight that it’s important we factor in ValueTask.

I hope that my contribution here helped the forward progress of this API, and I didn’t cause unwillingly a regression.

Thanks for the discussion.

I believe the options that return a ConfiguredTaskAwaitable<TResult> (or similar) were already dismissed as too risky when the task fails. There is one very specific implementation in dotnet/runtime: https://github.com/dotnet/runtime/blob/62e87b43207a17caa7b3d3260c1f58f155f5284e/src/libraries/System.Private.CoreLib/src/System/Threading/SemaphoreSlim.cs#L738 For general use I propose just returning the task as the result of the awaitable:

/// <summary>
/// Returns an awaitable that does not throw exceptions.
/// The caller is responsible for observing any exceptions.
/// </summary>
public static NoThrowAwaiter<T> NoThrow<T>(this T task) where T : Task => new(task);

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

This has the additional benefit of not needing to store the task in a captured variable (it’s instead only temporarily stored in the awaiter variable, which is anyway needed). A different overload+struct could add continueOnCapturedContext parameter support also.

Turns out it is possible to extend the existing ConfiguredTaskAwaitable structs so that they store a ConfigureAwaitBehavior enum instead of the current continueOnCapturedContext boolean without breaking changes or significantly impacting the hot path for async methods.

I’m a little surprised about that. I’d have expected the Asynchronous flag to need to be checked in IsCompleted and the NoThrow flag to need to be checked in GetResult, resulting in several more branches on the hot path as well as either more code that gets inlined into lots of call sites or methods that once were inlined no longer being so.

Running the existing async method benchmarks seems to confirm that there is no observable perf regression when compared with the current main branch.

Those don’t use ConfigureAwait.

strictly speaking it is pretending to not be completed

The definition of IsCompleted is up to the awaiter. In this case, that definition includes not supporting synchronous completion. It’s also in the System.Runtime.CompilerServices namespace because it’s part of the async/await infrastructure and meant to be consumed by the compiler via a very specific pattern. IsCompleted is a simple, understandable name, but if it helps you sleep better 😃, think of it as “StateMachineShouldProceedWithoutHookingUpACallback”.

Alternatively, we could perhaps consider adding an analyzer

Sounds reasonable.

force asynchrony by having the awaiter’s IsCompleted property always return false, this feels like a hack to me.

That’s exactly what it would do. I don’t see why it’s a hack; that’s the purpose of the IsCompleted property.

I mean, strictly speaking it is pretending to not be completed so that the state machine is forced to pass its continuation. I’d probably be less concerned if this weren’t a public property.

Also, I don’t understand what TResult? would mean:

I was motivated by the assumption that returning default for non-nullable reference types is more harmful than value types. But then again value types can contain references so probably not that important. Alternatively, we could perhaps consider adding an analyzer/fixer that detects statements of the form Foo foo = await task.ConfigureAwait(AwaitBehavior.NoThrow); and recommends changing those to Foo? foo = await task.ConfigureAwait(AwaitBehavior.NoThrow);.

If a developer writes _ = task.ConfigureAwait(ConfigureAwaitBehavior.NoThrow) without awaiting

Oh, wait, “without awaiting”? If there’s no await, the above is entirely a nop.

I prefer the ConfigureAwait approach, e.g. await task.ConfigureAwait(ConfigureAwaitBehavior.NoThrow | ConfigureAwaitBehavior.NoCapturedContext);, rather than having lots of different methods for all the various ways we might want to support awaiting and that all need to compose in some fashion (this is why the method is called ConfigureAwait today, because the idea was it could be extended in this manner in the future). This also ties in with #27723, for example, where we’d have an overload that would let you do await task.ConfigureAwait(timeout), an overload that would let you pass a cancellation token, an overload that would let you pass a timeout and a cancellation token and a ConfigureAwaitBehavior, etc. It all ends up just being about configuring the await. #32030

@eiriktsarpalis, are you still driving the design of this?