Proper way to deal with exceptions in AsyncDispose

Maybe you already understand why this happens, but it's worth spelling out. This behaviour isn't specific to await using. It would happen with a plain using block too. So while I say Dispose() here, it all applies to DisposeAsync() too.

A using block is just syntactical sugar for a try/finally block, as the remarks section of the documentation says. What you see happens because the finally block always runs, even after an exception. So if an exception happens, and there is no catch block, the exception is put on hold until the finally block runs, and then the exception is thrown. But if an exception happens in finally, you will never see the old exception.

You can see this with this example:

try {
    throw new Exception("Inside try");
} finally {
    throw new Exception("Inside finally");
}

It doesn't matter whether Dispose() or DisposeAsync() is called inside the finally. The behaviour is the same.

My first thought is: don't throw in Dispose(). But after reviewing some of Microsoft's own code, I think it depends.

Take a look at their implementation of FileStream, for example. Both the synchronous Dispose() method, and DisposeAsync() can actually throw exceptions. The synchronous Dispose() does ignore some exceptions intentionally, but not all.

But I think it's important to take into account the nature of your class. In a FileStream, for example, Dispose() will flush the buffer to the file system. That is a very important task and you need to know if that failed. You can't just ignore that.

However, in other types of objects, when you call Dispose(), you truly have no use for the object anymore. Calling Dispose() really just means "this object is dead to me". Maybe it cleans up some allocated memory, but failing doesn't affect the operation of your application in any way. In that case, you might decide to ignore the exception inside your Dispose().

But in any case, if you want to distinguish between an exception inside the using or an exception that came from Dispose(), then you need a try/catch block both inside and outside of your using block:

try {
    await using (var d = new D())
    {
        try
        {
            throw new ArgumentException("I'm inside using");
        }
        catch (Exception e)
        {
            Console.WriteLine(e.Message); // prints I'm inside using
        }
    }
} catch (Exception e) {
    Console.WriteLine(e.Message); // prints I'm inside dispose
}

Or you could just not use using. Write out a try/catch/finally block yourself, where you catch any exception in finally:

var d = new D();
try
{
    throw new ArgumentException("I'm inside try");
}
catch (Exception e)
{
    Console.WriteLine(e.Message); // prints I'm inside try
}
finally
{
    try
    {
        if (D != null) await D.DisposeAsync();
    }
    catch (Exception e)
    {
        Console.WriteLine(e.Message); // prints I'm inside dispose
    }
}

using is effectively Exception Handling Code (syntax sugar for try...finally...Dispose()).

If your exception handling code is throwing Exceptions, something is royally busted up.

Whatever else happened to even get you into there, does not really mater anymore. Faulty Exception handling code will hide all possible exceptions, one way or the other. The exception handling code must be fixed, that has absolute priority. Without that, you never get enough debugging data for the real problem. I see it done wrong extremly often. It is about as easy to get wrong, as handling naked pointers. So often, there are two Articles on the thematic I link, that might help you with any underlying design missconceptions:

  • A classification of Exceptions and wich you should catch
  • General Good Practices that the classification could not cover

Depending on the Exception classification, this is what you need to do if your Exception Handling/Dipose code throws an Exception:

For Fatal, Boneheaded and Vexing the solution is the same.

Exogenous Exceptions, must be avoided even at serious cost. There is a reason we still use logfiles rather then logdatabases to log exceptions - DB Opeartions are just way to prone to run into Exogenous problems. Logfiles are the one case, where I do not even mind if you keep the File Handle Open the entire Runtime.

If you got to close a connection, do not worry to much about the other end. Handle it like UDP does: "I will send the information, but I do not care if the other side get's it." Disposing is about cleaning up resources on the client side/side you are working on.

I can try to notify them. But cleaning up stuff on the Server/FS Side? That is what their timeouts and their exception handling is responsible for.


There are exceptions that you want to surface (interrupt the current request, or bring down the process), and there are exceptions that your design expects will occur sometimes and you can handle them (e.g. retry and continue).

But distinguishing between these two types is up to the ultimate caller of the code - this is the whole point of exceptions, to leave the decision up to the caller.

Sometimes the caller will place greater priority on surfacing the exception from the original code block, and sometimes the exception from the Dispose. There is no general rule for deciding which should take priority. The CLR is at least consistent (as you've noted) between the sync and non-async behaviour.

It's perhaps unfortunate that now we have AggregateException to represent multiple exceptions, it can't be retrofitted to solve this. i.e. if an exception is already in flight, and another is thrown, they are combined into an AggregateException. The catch mechanism could be modified so that if you write catch (MyException) then it will catch any AggregateException that includes an exception of type MyException. There are various other complications stemming from this idea though, and it's probably too risky to modify something so fundamental now.

You could improve your UsingAsync to support early return of a value:

public static async Task<R> UsingAsync<T, R>(this T disposable, Func<T, Task<R>> task)
        where T : IAsyncDisposable
{
    bool trySucceeded = false;
    R result;
    try
    {
        result = await task(disposable);
        trySucceeded = true;
    }
    finally
    {
        if (trySucceeded)
            await disposable.DisposeAsync();
        else // must suppress exceptions
            try { await disposable.DisposeAsync(); } catch { }
    }
    return result;
}