Does async Task method never throw?

It appears that an async method always captures its exceptions into a Task that the method returns including those thrown before the first await". In other words the following code works as intended and no exceptions are thrown until the line with Task.WhenAll:

async Task DoSomethingAsync(int i)
{
       if (i == 2) throw new InvalidOperationException();
       await Task.Delay(1000);
} 

...

var tasks = new List<Task>();
for(int i = 0; i < 3; ++i)
{
     var t = DoSomethingAsync(i); // no awaits here
     tasks.Add(t); 
} 


// Wait for all tasks
await Task.WhenAll(tasks);   // throws  InvalidOperation when the other 2 tasks succeed

The question:

Is this behavior of async methods part of the language spec or it is just the way it was implemented in the current version of .NET? Can I rely on this behavior in my code?

Jon Skeet
people
quotationmark

Is this behavior of async methods part of the language spec

Yes it is. From section 10.15.1 of the C# 5 specification:

  • If the function body terminates as the result of an uncaught exception (§8.9.5) the exception is recorded in the return task which is put into a faulted state.

Section 10.15.2 gives details of async methods returning void:

If the return type of the async function is void, evaluation differs from the above in the following way: Because no task is returned, the function instead communicates completion and exceptions to the current thread’s synchronization context. The exact definition of synchronization context is implementation-dependent, but is a representation of “where” the current thread is running. The synchronization context is notified when evaluation of a void-returning async function commences, completes successfully, or causes an uncaught exception to be thrown.

Admittedly I don't think it explicitly says that the call itself doesn't throw, but I believe that is the intended interpretation of the spec.

If you want a method to throw if (say) preconditions aren't met, but then use normal async behaviour, you can take the same approach as is common for iterator blocks:

public Task Foo(int someParameter)
{
    // Check preconditions here and throw - note that this isn't an
    // async method, so the exceptions will be thrown synchronously.
    return FooImpl(someParameter);
}

private async Task FooImpl(int someParameter)
{
    // Assume everything is valid now.
    // Normal async method implementation.
}

people

See more on this question at Stackoverflow