Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
668 views
in Technique[技术] by (71.8m points)

c# - Catching Exceptions in async methods when not called with await

Goal:

I am confused by the behavior I am seeing with exceptions in my .Net Core library. The goal of this question is to understand why it is doing what I am seeing.

Executive Summary

I thought that when an async method is called, the code in it is executed synchronously until it hits the first await. If that is the case, then, if an exception is thrown during that "synchronous code", why is it not propagated up to the calling method? (As a normal synchronous method would do.)

Example Code:

Given the following code in a .Net Core Console Application:

static void Main(string[] args)
{
    Console.WriteLine("Hello World!");

    try
    {
        NonAwaitedMethod();
    }
    catch (Exception e)
    {
        Console.WriteLine("Exception Caught");
    }

    Console.ReadKey();
}

public static async Task NonAwaitedMethod()
{
    Task startupDone = new Task(() => { });
    var runTask = DoStuff(() =>
    {
        startupDone.Start();
    });
    var didStartup = startupDone.Wait(1000);
    if (!didStartup)
    {
        throw new ApplicationException("Fail One");
    }

    await runTask;
}

public static async Task DoStuff(Action action)
{
    // Simulate starting up blocking
    var blocking = 100000;
    await Task.Delay(500 + blocking);
    action();
    // Do the rest of the stuff...
    await Task.Delay(3000);
}

}

Scenarios:

  1. When run as is, this code will throw an exception, but, unless you have a break point on it, you will not know it. The Visual Studio Debugger nor the Console will give any indication that there was an issue (aside from a one line note in the Output screen).

  2. Swap the return type of NonAwaitedMethod from Task to void. This will cause the Visual Studio Debugger to now break on the exception. It will also be printed out in the console. But notably, the exception is NOT caught in the catch statement found in Main.

  3. Leave the return type of NonAwaitedMethod as void, but take off the async. Also change the last line from await runTask; to runTask.Wait(); (This essentially removes any async stuff.) When run, the exception is caught in the catch statement in the Main method.

So, to summarize:

| Scenario   | Caught By Debugger | Caught by Catch |  
|------------|--------------------|-----------------|  
| async Task | No                 | No              |  
| async void | Yes                | No              |  
| void       | N/A                | Yes             |  

Questions:

I thought that because the exception was thrown before an await was done, that it would execute synchronously up to, and through the throwing of the exception.

Hence my question of: Why does neither scenario 1 or 2 get caught by the catch statement?

Also, why does swapping from Task to void return type cause the exception to get caught by the Debugger? (Even though I am not using that return type.)

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

exception was thrown before an await was done, that it would execute synchronously

Thought this is fairly true, but it doesn't mean you could catch the exception.

Because your code has async keyword, which turns the method into an async state machine i.e. encapsulated / wrapped by a special type. Any exception thrown from async state machine will get caught and re-thrown when the task is awaited (except for those async void ones) or they go unobserved, which can be caught in TaskScheduler.UnobservedTaskException event.

If you remove async keyword from the NonAwaitedMethod method, you can catch the exception.

A good way to observe this behavior is using this:

try
{
    NonAwaitedMethod();

    // You will still see this message in your console despite exception
    // being thrown from the above method synchronously, because the method
    // has been encapsulated into an async state machine by compiler.
    Console.WriteLine("Method Called");
}
catch (Exception e)
{
    Console.WriteLine("Exception Caught");
}

So your code is compiled similarly to this:

try
{
    var stateMachine = new AsyncStateMachine(() =>
    {
        try
        {
            NonAwaitedMethod();
        }
        catch (Exception ex)
        {
            stateMachine.Exception = ex;
        }
    });

    // This does not throw exception
    stateMachine.Run();
}
catch (Exception e)
{
    Console.WriteLine("Exception Caught");
}

why does swapping from Task to void return type cause the exception to get caught

If the method returns a Task, the exception is caught by the task.

If the method is void, then the exception gets re-thrown from an arbitrary thread pool thread. Any unhandled exception thrown from thread pool thread will cause the app to crash, so chances are the debugger (or maybe the JIT debugger) is watching this sort of exceptions.

If you want to fire and forget but properly handle the exception, you could use ContinueWith to create a continuation for the task:

NonAwaitedMethod()
    .ContinueWith(task => task.Exception, TaskContinuationOptions.OnlyOnFaulted);

Note you have to visit task.Exception property to make the exception observed, otherwise, task scheduler still will receive UnobservedTaskException event.

Or if the exception needs to be caught and processed in Main, the correct way to do that is using async Main methods.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...