Martin Liversage
Martin Liversage

Reputation: 106926

Surprising case where exception handling in async method does not catch the exception

My code combines async methods with exception handling. The code is very simple and all tasks are awaited and there are no async void methods:

async Task DoWorkSafelyWithStateMachine()
{
    try
    {
        await DoWorkThatMightThrowException();
    }
    catch (Exception exception)
    {
        Console.WriteLine("With state machine: " + exception.Message);
    }
}

Awaiting this method does not throw an exception because the exception was swallowed:

await DoWorkSafelyWithStateMachine(); // No exception thrown

However, the code does not always catch exceptions as it is supposed to do. The problem arises when the method is written in a slightly different way where no async state machine is created by the compiler:

Task DoWorkSafelyWithoutStateMachine()
{
    try
    {
        return DoWorkThatMightThrowException();
    }
    catch (Exception exception)
    {
        Console.WriteLine("Without state machine: " + exception.Message);
        return Task.CompletedTask;
    }
}

The method is not decorated with async and nothing is awaited inside the method. Instead the task returned by the method inside try is returned to the caller. However, in my experience the magic of the compiler somehow still ensures that if the method inside try throws an exception it gets caught by the exception handler. Well, apparently that is not always true.

To test these two variations of the same method I let DoWorkThatMightThrowException throw an exception. If the method does not have to use await in the body of the method then it can be implemented either with or without an async state machine:

async Task DoWorkThatMightThrowExceptionWithStateMachine()
{
    throw new Exception("With state machine");
    await Task.CompletedTask;
}

Task DoWorkThatMightThrowExceptionWithoutStateMachine()
{
    throw new Exception("Without state machine");
    return Task.CompletedTask;
}

I have discovered that calling DoWorkThatMightThrowExceptionWithStateMachine from DoWorkSafelyWithoutStateMachine does not catch the exception thrown. The other three combinations do catch the exception. In particular, the version where no async state machines are involved in either method catches the exception and I have mistakenly extrapolated this observation with the unfortunate result that some of my code now has subtle errors.

                          | Throw + state machine | Throw - state machine |
--------------------------+-----------------------+-----------------------+
Try/catch + state machine |        Caught         |        Caught         |
Try/catch - state machine |      Not caught       |        Caught         |

Doing this experiment I have learned that I always have to await a task inside a try block (first line in table). However, I don't understand the inconsistency in the second row of the table. Please explain this behavior. Where is it documented? It is not easy to search for information about this. The more basic problem of exceptions being "lost" because tasks are not awaited will dominate the search results.

Upvotes: 3

Views: 3266

Answers (2)

René Vogt
René Vogt

Reputation: 43936

I don't understand the inconsistency in the second row of the table

The right side case (no state machines at all) is trivial:

  • you call a method inside a try/catch block
  • the method throws an exception
  • your catch black catches it - nothing special

The left side is actually easy to understand, too:

  • you call a method inside a try/catch block
  • but this method is not what it seems, it has been converted to a state machine, so what it returns is a Task that represents the execution of the method's content as you implemented it(1)
  • so as long as you don't Wait or await that returned Task or try to access its Result property, the exception is not (re-)thrown inside your try block.

(1) I wish my English was better to find a better and more exact description. As Servy pointed out, in your example an already faulted Task is returned.

Upvotes: 4

Servy
Servy

Reputation: 203812

The catch block will execute when an exception is thrown in the try block. When you write return DoWorkThatMightThrowExceptionWithStateMachine(); all the try block is doing is constructing a Task marked as faulted and returning it. No exception is ever thrown, so no catch block is ever run.

If you await the faulted task it will re-throw that exception, so there is an exception being thrown. When you call DoWorkThatMightThrowExceptionWithoutStateMachine the method itself is throwing an exception, not returning a faulted task, and so there's an exception being thrown in the try block.

Upvotes: 5

Related Questions