Runtime: Support await'ing a Task without throwing

Created on 6 Jun 2017  路  10Comments  路  Source: dotnet/runtime

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:
```C#
try { await task; } catch { }

but that incurs the cost of the throw and also triggers first-chance exception handling.  You can use a continuation:
```C#
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:
```C#
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:
```C#
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:
```C#
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:
```C#
await task.ConfigureAwait(ConfigureAwaitBehavior.NoThrow);

or that wants to have the equivalent of ConfigureAwait(false) and also not throw:
```C#
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.
```C#
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:
C# 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.

api-needs-work area-System.Threading.Tasks

Most helpful comment

Change ConfigureAwaitBehavior to AwaitBehavior?

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

All 10 comments

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.

馃挱 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.

I really wish there was a way to return a cancelled result from an async method without throwing the exception...

That's unrelated to this issue, though. You're talking about how you transition the returned Task to be in a canceled state. This issue is about the consuming side, regardless of how the Task-returning method was implemented... it may not have been using async/await at all.

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)

Rather than NoCapturedContext, I would prefer to see

But that means that code like:
C# await task.ConfigureAwait(ConfigureAwaitBehavior.NoThrow);
would end up behaving like continueOnCapturedContext==false, and thus not incorporating the default for that option.

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?

...which is the exact reason Clean Code (and a lot of similar books/posts) recommends against them.

And if I ever get around to finishing that writeup of the language idea I have, raw Boolean parameters would be explicitly disallowed, in favor of some encapsulating value (like a shortcut enum declaration or something)

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

I think the question here is whether it's better to have clean and simple API or whether it's better if the code that uses the API is simple and short.

With task.ConfigureAwait(ConfigureAwaitBehavior.NoCapturedContext | ConfigureAwaitBehavior.NoThrow) the API is simple, easy to understand and easy to extend, but the code is very verbose (though using static can improve that somewhat).

On the other hand, with something like task.NoCapturedContext().NoThrow(), the code is simpler and much shorter, but the API becomes much more complicated (two methods on Task and the same two methods on ConfiguredTaskAwaitable).

Change ConfigureAwaitBehavior to AwaitBehavior?

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

On the other hand, with something like task.NoCapturedContext().NoThrow(), the code is simpler and much shorter, but the API becomes much more complicated (two methods on Task and the same two methods on ConfiguredTaskAwaitable).

If that's really all the choice is, then it seems to me one should almost always favour putting the complexity in the framework rather than the consuming app - otherwise what's the framework for?

If everything has to be duplicated on Task and ConfiguredTaskAwaitable then that's a real pain and might be a showstopper, but within a single class there can't be much to choose in terms of complexity/LoC between an API with a method which tears up a bitfield enum of loosely-related flags and calls helpers for each flag vs. an API which exposes the individual helpers directly.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

jchannon picture jchannon  路  3Comments

jzabroski picture jzabroski  路  3Comments

nalywa picture nalywa  路  3Comments

matty-hall picture matty-hall  路  3Comments

GitAntoinee picture GitAntoinee  路  3Comments