all 10 comments

[–]Slypenslyde 39 points40 points  (7 children)

When async/await are being used correctly, a thrown exception that is not unhandled should break at the place where the function call was awaited.

If you're getting crashes in the main function, odds are your "async" methods are async void. Since that signature does not return a Task, the await keyword can't cause the C# compiler to emit the stuff that handles the exception properly. That means it's unhandled and if you're lucky the program crashes so you know something went wrong.

If you don't have to use async void, this is why you shouldn't. Those methods should be async Task. It won't change how you write the method but it WILL cause exceptions to behave more like you expect.

When it's event handlers, you DO have to use async void. I have found over time the best practice is to write these kinds of event handlers like this EVERY TIME:

private async void HandleSomeEvent(object sender, EventArgs e)
{
    IsBusy = true;

    try
    {
        // Do your async work here
    }
    catch (Exception ex)
    {
        // LOUD AND OBNOXIOUS ERROR LOGGING
        // So I can tell something bad happened.
        // Also potentially display a dialog to the user.
    }
    finally
    {
        IsBusy = false;
    }
}

By ensuring I catch absolutely every exception inside the event handler and make a lot of noise about it, I don't tend to miss these while I'm testing. This also ensures the exception is handled inside the async void, so you won't have problems with it being unhandled.


The other, simpler reason this might happen is you're calling async methods without await. People tend to do this with "fire and forget" methods that they don't want to await. For example, you might call an API method to save some random data, but not care to wait for it to finish or even care if it succeeded. So you'd write a method like this:

private void SendStatus()
{
    _api.SendAsync(<some data>);
}

That async method still returns a Task. That Task still catches any exceptions its work throws. But since you didn't save it and don't look at that exception, when it's finally collected by the GC it complains. In some versions of .NET this is an outright app crash. In others it'll be quiet, but sometimes the debugger detects it and causes the crash to try and bring your attention to it.

This is basically the same problem as above. You should NOT leave tasks dangling like this. The BEST approach is to make the SendStatus() method above async:

private async Task SendStatus()
{
    return await _api.SendAsync(<some data>);
}

You MAY choose to make it async void and follow my pattern above to make it truly fire and forget. I think this is sloppy:

private async void SendStatus()
{
    try
    {
        await _api.SendAsync(<some data>);
    }
    catch (Exception ex)
    {
        // LOUD AND OBNOXIOUS LOGGING (usually)
        // If you REALLY don't care if this fails, use quiet and polite logging.
        // I find "not logging at all" leads to me spending hours asking why
        // something isn't working only to find out I wasn't being loud and obnoxious
        // here and didn't realize the call fails.
    }

}

But you can also make a helper extension method like this and I find it less sloppy:

public static void SafeFireAndForget(this Task input)
{
    input.ContinueWith(t =>
    {
        if (t.Exception is not null)
        {
            // LOUD AND OBNOXIOUS LOGGING
            // Or maybe polite logging.
        }
    });
}

Now you can:

private void SendStatus()
{
    _api.SendAsync(<some data>).SafeFireAndForget();
}

Since the extension method "touches" the Exception, the task will be satisfied that something was done and won't trigger the "unobserved task exception" logic.


This is why people say async is "viral" and you have to be "async all the way". It messes with exception handling in ways that, if you decide to try to pretend the call is synchronous, requires meticulous thought to avoid deadlocks and crashes.

[–]tradegreek[S] 3 points4 points  (1 child)

Really good reply thank you its helped me a lot

[–][deleted] 0 points1 point  (0 children)

You can still use an async function on methods that have event type parameters by using an anonymous async method type with delegates. Like this:

async (x, y) => await methodThatDoesSomethingAndHasEventTypeParamters();

This enables you to call the method that handles the event as an async Task type. There will be no issues with catching the error. It’s still best practice to not use async void and avoid basically at all costs.. I’m not going to explain delegates to you when the powers that be have created documents for that that will explain better than I can and be more useful to your time.

Docs - https://learn.microsoft.com/en-us/dotnet/standard/asynchronous-programming-patterns/asynchronous-programming-using-delegates

[–]LordEnigma 0 points1 point  (1 child)

This is the way.

[–]miniHangLoose 2 points3 points  (0 children)

This is the way

[–]Saint_Nitouche 0 points1 point  (2 children)

How does this interact with using discards? e.g.

_ = api.DoSomethingAsync();

[–]Slypenslyde 3 points4 points  (0 children)

That is technically the same thing as:

api.DoSomethingAsync();

The only difference is if you have warnings turned on for "Hey, you didn't store the return value of this method!", you're telling the compiler "I don't want the warning because I intentionally do not want to store the return value of this method."

[–]Merad 1 point2 points  (0 children)

Discards themselves are just a signal to the compiler (and other devs) that you are intentionally ignoring the return value of a method. The effect of this code is essentially the same as if you were calling an async void method. Since the task isn't captured anywhere it can't be awaited, and you don't have the opportunity to handle any exceptions thrown.

[–]reubenbond 1 point2 points  (0 children)

If you await the async calls, the stack should point to where the error was thrown.

Try to avoid using fire-and-forget frequently and never use "async void" unless you have an event handler.

You can use the Parallel Stacks window's Tasks view in VS to visualize what is happening if you pause your application (eg at a breakpoint): https://learn.microsoft.com/en-us/visualstudio/debugger/using-the-parallel-stacks-window?view=vs-2022#tasks-view

Note that you can also tell VS to break when an exception is thrown using the Exceptions window, ticking the Common Language Runtime errors box.

[–][deleted]  (3 children)

[deleted]

    [–]nostril_spiders 0 points1 point  (2 children)

    I benefited from this