Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does Exception from async void crash the app but from async Task is swallowed

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(); }
like image 942
ispiro Avatar asked Nov 08 '18 20:11

ispiro


People also ask

What happens if an exception is thrown within an asynchronous method?

As we know, in asynchronous programming, control does not wait for the function's result and it executes the next line. So when the function throws an exception, at that moment the program control is out of the try-catch block.

What is the difference between async void and async task?

A Task returning async method can be awaited, and when the task completes, the continuation of the task is scheduled to run. A void returning async method cannot be awaited; it is a "fire and forget" method. It does work asynchronously, and you have no way of telling when it is done.

What happens if we execute an asynchronous method but don't await it?

The call to the async method starts an asynchronous task. However, because no Await operator is applied, the program continues without waiting for the task to complete. In most cases, that behavior isn't expected.

Can async return void?

In short, if your async method is an event handler or a callback, it's ok to return void .


2 Answers

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).

like image 155
dymanoid Avatar answered Sep 20 '22 14:09

dymanoid


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.

like image 36
Alex Fanat Avatar answered Sep 16 '22 14:09

Alex Fanat