Why does Exception from async void crash the app b

2020-02-11 03:06发布

I understand that an async Task's Exceptions can be caught by:

try { await task; }
catch { }

while an async void's cannot because it cannot be awaited.

But why is it that when the async Task is not awaited (just like the async void one) the Exception is swallowed, while the void's one crashes the application?

Caller: ex();

Called:

async void ex() { throw new Exception(); }
async Task ex() { throw new Exception(); }

3条回答
神经病院院长
2楼-- · 2020-02-11 03:13

Please read important note at bottom.

The async void method will crash the application because there is no Task object for the C# compiler to push the exception into. On a functional level, the async keyword on a Task-returning method is just heavy syntax sugar that tells the compiler to rewrite your method in terms of a Task object using the various methods available on the object as well as utilities such as Task.FromResult, Task.FromException, and Task.FromCancelled, or sometimes Task.Run, or equivalents from the compiler's point of view. This means that code like:

async Task Except()
{
    throw new Exception { };
}

gets turned into approximately:

Task Except()
{
    return Task.FromException(new Exception { });
}

and so when you call Task-returning async methods that throw, the program doesn't crash because no exception is actually being thrown; instead, a Task object is created in an "excepted" state and is returned to the caller. As mentioned previously, an async void-decorated method doesn't have a Task object to return, and so the compiler does not attempt to rewrite the method in terms of a Task object, but instead only tries to deal with getting the values of awaited calls.

More Context

Task-returning methods can actually cause exceptions too, even when not being awaited because the async keyword is what causes the swallowing, so if it is not present, the exceptions in the method will not be swallowed, such as the following.

Task Except() // Take note that there is no async modifier present.
{
    throw new Exception { }; // This will now throw no matter what.
    return Task.FromResult(0); // Task<T> derives from Task so this is an implicit cast.
}

The reason why awaiting a call will actually throw the exception supposedly thown in a Task-returning async method is because the await keyword is supposed to throw the swallowed Exceptions by design to make debugging easier in an asynchronous context.

Important Note

The way that these "rewrites" are actually processed by the compiler and manifested by the compiled code may be different than how I have implied, but are roughly equivalent on a functional level.

查看更多
叛逆
3楼-- · 2020-02-11 03:21

Because, your methods are not executed asynchronously.

Execution will run synchronously until it "meet" await keyword.

So in case of void application will throw an exception, because exception occurs in the current execution context.

In case of Task even exception thrown synchronously it will be wrapped in the Task and returned to the caller.

You should get desired behaviour with void as well if you will use await in the function.

async void Ex()
{
    await Task.Delay(1000);
    throw new Exception();
}
查看更多
We Are One
4楼-- · 2020-02-11 03:27

TL;DR

This is because async void shouldn't be used! async void is only there to make legacy code work (e.g. event handlers in WindowsForms and WPF).

Technical details

This is because of how the C# compiler generates code for the async methods.

You should know that behind async/await there's a state machine (IAsyncStateMachine implementation) generated by the compiler.

When you declare an async method, a state machine struct will be generated for it. For your ex() method, this state machine code will look like:

void IAsyncStateMachine.MoveNext()
{
    try
    {
        throw new Exception();
    }
    catch (Exception exception)
    {
        this.state = -2;
        this.builder.SetException(exception);
    }
}

Note that this.builder.SetException(exception); statement. For a Task-returning async method, this will be an AsyncTaskMethodBuilder object. For a void ex() method, it will be an AsyncVoidMethodBuilder.

The ex() method body will be replaced by the compiler with something like this:

private static Task ex()
{
    ExAsyncStateMachine exasm;
    exasm.builder = AsyncTaskMethodBuilder.Create();
    exasm.state = -1;
    exasm.builder.Start<ExAsyncStateMachine>(ref exasm);
    return exasm.builder.Task;
}

(and for the async void ex(), there will be no last return line)

The method builder's Start<T> method will call the MoveNext method of the state machine. The state machine's method catches the exception in its catch block. This exception should normally be observed on the Task object - the AsyncTaskMethodBuilder.SetException method stores that exception object in the Task instance. When we drop that Task instance (no await), we don't see the exception at all, but the exception itself isn't thrown anymore.

In the state machine for async void ex(), there's an AsyncVoidMethodBuilder instead. Its SetException method looks different: since there's no Task where to store the exception, it has to be thrown. It happens in a different way, however, not just a normal throw:

AsyncMethodBuilderCore.ThrowAsync(exception, synchronizationContext);

The logic inside that AsyncMethodBuilderCore.ThrowAsync helper decides:

  • If there's a SynchronizationContext (e.g. we're on a UI thread of a WPF app), the exception will be posted on that context.
  • Otherwise, the exception will be queued on a ThreadPool thread.

In both cases, the exception won't be caught by a try-catch block that might be set up around the ex() call (unless you have a special SynchronizationContext that can do this, see e.g. Stephen Cleary's AsyncContext).

The reason is simple: when we post a throw action or enqueue it, we then simply return from the ex() method and thus leave the try-catch block. Then, the posted/enqueued action is executed (either on the same or on a different thread).

查看更多
登录 后发表回答