22

During switching to the new .NET Core 3's IAsynsDisposable, I've stumbled upon the following problem.

The core of the problem: if DisposeAsync throws an exception, this exception hides any exceptions thrown inside await using-block.

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

class D : IAsyncDisposable
{
    public async ValueTask DisposeAsync()
    {
        await Task.Delay(1);
        throw new Exception("I'm inside dispose");
    }
}

What is getting caught is the DisposeAsync-exception if it's thrown, and the exception from inside await using only if DisposeAsync doesn't throw.

I would however prefer it other way round: getting the exception from await using block if possible, and DisposeAsync-exception only if the await using block finished successfully.

Rationale: Imagine that my class D works with some network resources and subscribes for some notifications remote. The code inside await using can do something wrong and fail the communication channel, after that the code in Dispose which tries to gracefully close the communication (e. g., unsubscribe from the notifications) would fail, too. But the first exception gives me the real information about the problem, and the second one is just a secondary problem.

In the other case when the main part ran through and the disposal failed, the real problem is inside DisposeAsync, so the exception from DisposeAsync is the relevant one. This means that just suppressing all exceptions inside DisposeAsync shouldn't be a good idea.


I know that there is the same problem with non-async case: exception in finally overrides the exception in try, that's why it's not recommended to throw in Dispose(). But with network-accessing classes suppressing exceptions in closing methods doesn't look good at all.


It's possible to work around the problem with the following helper:

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

and use it like

await new D().UsingAsync(d =>
{
    throw new ArgumentException("I'm inside using");
});

which is kind of ugly (and disallows things like early returns inside the using block).

Is there a good, canonical solution, with await using if possible? My search in internet didn't find even discussing this problem.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
Vlad
  • 35,022
  • 6
  • 77
  • 199
  • 1
    "*But with network-accessing classes suppressing exceptions in closing methods doesn't look good at all*" -- I think most networky BLC classes have a separate `Close` method for this very reason. It's probably wise to do the same: `CloseAsync` attempts to close things down nicely and throws on failure. `DisposeAsync` just does its best, and fails silently. – canton7 Nov 20 '19 at 12:11
  • @canton7: Well, having a separate `CloseAsync` means that I need to take extra precautions to get it running. If I just put it at the end of `using`-block, it will be skipped on early returns etc. (this is what we would want to happen) and exceptions (this is what we would want to happen). But the idea looks promising. – Vlad Nov 20 '19 at 12:14
  • There's a reason many coding standards forbid early returns :) Where networking is involved, being a bit explicit is no bad thing IMO. `Dispose` has always been "Things might have gone wrong: just do your best to improve the situation, but don't make it worse", and I don't see why `AsyncDispose` should be any different. – canton7 Nov 20 '19 at 12:16
  • @canton7: Well, in a language-with-exceptions every statement might be an early return :-\ – Vlad Nov 20 '19 at 12:17
  • Right, but those will be *exceptional*. In that case, making `DisposeAsync` do its best to tidy up *but not throw* is the right thing to do. You were talking about *intentional* early returns, where an intentional early return might mistakenly bypass a call to `CloseAsync`: those are the ones forbidden by many coding standards. – canton7 Nov 20 '19 at 12:19
  • @canton7: This can be the way to go, but changing the team coding guidelines from _endorsing_ early returns (and automatic resource deallocation with `using`) to _forbidding_ them doesn't seem to be a good choice. – Vlad Nov 20 '19 at 12:23
  • That's fair. I'm still of the opinion that `DisposeAsync` should do its best to tidy up without throwing though: that's how `Dispose` works. If you need a `CloseAsync` method which does throw on failure, write one. – canton7 Nov 20 '19 at 12:26
  • This is a design question concerning a feature of C# 8. IMO, the discussion better belongs to GitHub repo [here](https://github.com/dotnet/csharplang). P.S.: I completely agree with your concern, the work-around, and how it is supposed to work. – Tanveer Badar Nov 20 '19 at 13:19
  • @TanveerBadar: As far as I remember from silent reading the language design repo, it’s more the place for proposals than for clarification requests. – Vlad Nov 21 '19 at 20:03
  • It is a big no-no, code analysis rule CA1065. Anything you'll *try* to do about this makes the problem worse. So don't do it. – Hans Passant Nov 28 '19 at 13:59
  • @HansPassant: Ok so what kind of solution to the problem you propose? Leave everything as it is and suppress all the exceptions in `DisposeAsync`? Stop using `await using` and code everything with try/finally? Stop using `IAsyncDisposable` and code everything with try/finally? – Vlad Nov 28 '19 at 18:34
  • Related: [Should you implement IDisposable.Dispose() so that it never throws?](https://stackoverflow.com/questions/577607/should-you-implement-idisposable-dispose-so-that-it-never-throws) – Theodor Zoulias Jan 07 '22 at 13:38

4 Answers4

6

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;
}
Daniel Earwicker
  • 114,894
  • 38
  • 205
  • 284
  • So do I understand correct: your idea is that in some cases just standard `await using` can be used (this is where the DisposeAsync won't throw in non-fatal case), and a helper like `UsingAsync` is more appropriate (if DisposeAsync is likely to throw)? (Of course, I'd need to modify `UsingAsync` so that it doesn't blindly catch everything, but only non-fatal (and not-boneheaded in [Eric Lippert's usage](https://blogs.msdn.microsoft.com/ericlippert/2008/09/10/vexing-exceptions/)).) – Vlad Dec 06 '19 at 00:26
  • @Vlad yes - the correct approach is totally dependent on the context. Also note that UsingAsync cannot be written once to use some globally true categorisation of exception types according to whether they ought to be caught or not. Again this is a decision to be taken be differently depending on the situation. When Eric Lippert speaks of those categories, they are not intrinsic facts about exception types. The category per exception type depends upon your design. Sometimes an IOException is expected by design, sometimes not. – Daniel Earwicker Dec 06 '19 at 01:15
5

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
    }
}
Gabriel Luci
  • 38,328
  • 4
  • 55
  • 84
  • 3
    Btw, https://source.dot.net (.NET Core) / https://referencesource.microsoft.com (.NET Framework) is a lot easier to browse than GitHub – canton7 Nov 20 '19 at 14:09
  • Thank you for your answer! I know what the real reason is (I mentioned try/finally and synchronous case in the question). Now about your proposal. A `catch` _inside_ the `using` block would not help because usually exception handling is done somewhere far from the `using` block itself, so handling it inside `using` is usually not very practicable. About using no `using` — is it really better than the proposed workaround? – Vlad Nov 20 '19 at 14:12
  • 2
    @canton7 Awesome! I was aware of https://referencesource.microsoft.com, but didn't know there was an equivalent for .NET Core. Thanks! – Gabriel Luci Nov 20 '19 at 14:14
  • @Vlad "Better" is something only you can answer. I know if I was reading someone else's code, I would prefer seeing `try`/`catch`/`finally` block since it would be immediately clear what it's doing without having to go read what `AsyncUsing` is doing. You also keep the option of doing an early return. There will also an extra CPU cost to your `AwaitUsing`. It would be small, but it's there. – Gabriel Luci Nov 20 '19 at 14:20
  • `Dispose* should never throw. From [the docs](https://learn.microsoft.com/en-us/dotnet/standard/garbage-collection/implementing-dispose): "To help ensure that resources are always cleaned up appropriately, a Dispose method should be callable multiple times without throwing an exception." – Paulo Morgado Nov 20 '19 at 21:44
  • 2
    @PauloMorgado That just means that `Dispose()` should not throw *because* it is called more than once. Microsoft's own implementations can throw exceptions, and for good reason, as I've shown in this answer. However, I do agree that you should avoid it if at all possible as no one would normally expect it to throw. – Gabriel Luci Nov 20 '19 at 21:53
  • @GabrielLuci, there's no such thing as "Microsoft's own implementations". Something was done by someone at some point in time. In the early days, Microsoft developers were C++ developers. not C#/.NET developers and the rules were still being written. This question shows what a `Dispose` method throwing can do. – Paulo Morgado Nov 20 '19 at 23:40
  • @PauloMorgado: I doubt that the idea of throwing in `Dispose` comes from C++ background, since in C++ throw in destructor (C++ counterpart of throwing in finally with RAII) leads to program termination: [1](https://en.cppreference.com/w/cpp/language/destructor#Exceptions), [2](https://ideone.com/T76LWo). – Vlad Nov 21 '19 at 10:40
  • @vlad, not what I said. And the same should happen in `Dispose` - program termination. – Paulo Morgado Nov 21 '19 at 11:08
3

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:

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.

Christopher
  • 9,634
  • 2
  • 17
  • 31
  • So your proposal effectively boils down to suppressing exceptions on connection close, right? – Vlad Dec 02 '19 at 17:22
  • 1
    @Vlad Exogenous ones? Sure. Dipose/Finalizer are there to clean up on their own side. Chances are when closing the Conneciton Instance due to exception, you do so becaue you no longer *have* a working connection to them anyway. And what point would there be in getting a "No connection" Exception while handling the previous "No Connection" Exception? You send a single "Yo, I a closing this connection" where you ignore all exogenous Exceptions or even if it get close to the target. Afaik the default implementations of Dispose already do that. – Christopher Dec 02 '19 at 20:27
  • @Vlad: I remebered, that there are bunch of things you are never supposed to throw exceptions (Except of coruse Fatal ones) from. Type Initliaizers are way up on the list. Dispose is also one of those: "To help ensure that resources are always cleaned up appropriately, a Dispose method should be callable multiple times without throwing an exception." https://learn.microsoft.com/en-us/dotnet/standard/garbage-collection/implementing-dispose – Christopher Dec 02 '19 at 20:57
  • @Vlad The Chance of Fatal Exceptions? We alwasy have to risk those and should never handle them beyond "call Dispose". And should not really do anything with those. They in fact go without mention in any documentation. | Boneheaded Exceptions? Always fix them. | Vexing exceptions are prime candidates for swallowing/handling, like in TryParse() | Exogenous? Also should always be handeled. Often you also want to tell the user about them and log them. But otherwise, they are not worth killing your process over. – Christopher Dec 02 '19 at 21:02
  • @Vlad I looked up SqlConnection.Dispose(). It does not even *care* to send anything to the Server about the connection being over. Something might still happen as result of the `NativeMethods.UnmapViewOfFile();` and `NativeMethods.CloseHandle()`. But those are imported from extern. There is no checking of any return value or anything else that could be used to get a proper .NET Exception around whatever those two might encounter. So I am strongly asuming, SqlConnection.Dispose(bool) simply does not care. | Close is a lot nicer, actually telling the server. Before it calls dispose. – Christopher Dec 02 '19 at 21:10
  • Okay, I see value in differentiating between the different exception types. Of course, fatal exceptions should never be suppressed, no matter what happens, and vexing exceptions are to be avoided. Boneheaded exceptions are a problem, because they do mean bugs in my code, so suppressing them seems to be a bad idea anyway. – Vlad Dec 05 '19 at 09:46
  • But for exogenous exceptions, if I would choose to suppress them, wouldn't it be a problem if the "main" code (the one within try/using block) _doesn't_ throw? In case of exogenous exception the communication channel is broken, and most probably other code communicating through the same channel isn't going to work anyway. Shouldn't we better keep the exception from finally in this case? – Vlad Dec 05 '19 at 09:49
  • @Vlad Again: The reasons you are even **in** Dipose right now are: a) You got a exception beforehand b) You no longer need this connection/resource anymore. At this point, exogenous exceptions stoped mattering. At any point before that they are very important - so you get into your Dispose code. | Let us keep at the DB example: What would be the *point* of SqlConnection.Dispose() throwing "the connection is closed"? – Christopher Dec 06 '19 at 22:34
  • It's true that the exceptions don't matter if I land in `Dispose` due to an exception tearing down my connection. But what if I land in `Dispose` in a usual way, with no exception in `using` block? Is suppressing an exception a good thing in this case? I doubt this. – Vlad Dec 20 '19 at 11:18
  • @Vlad "But what if I land in Dispose in a usual way, with no exception in using block?" Then you no longer need the connection, and it being broken is no longer relevant for you. Again I looked at SQL Connections code for Dispose(). SQLConnection is the *book example* for Dispose in general and using in particular. | It does not mater how (return, exception, got) or why you passed out of the using block - you no longer need the connection. It must be reliably disposed. And by design, Dispose can be called as often as you want too. – Christopher Dec 21 '19 at 07:56
  • It's true that I don't need the exception in _some_ setups, for example when I am closing the connection. (Although some may argue that with this approach the software is predestined to [always operate in failure mode](https://devblogs.microsoft.com/oldnewthing/20080416-00/?p=22723).) But imagine that the the dispose isn't closing the connection but is merely unsubscribing from the remote events. So after the unsubscription I _still need_ the connection. If however the connection is broken as a result of the unsubscription, suppressing the exception starts being dangerous. – Vlad Dec 25 '19 at 16:36
  • In general, after several more years of experience I find your idea a good one: ignoring exogenous exceptions in AsyncDispose is the smallest of all evils. – Vlad May 21 '21 at 20:44
1

You can try use AggregateException and modify your code something like this:

class Program 
{
    static async Task Main()
    {
        try
        {
            await using (var d = new D())
            {
                throw new ArgumentException("I'm inside using");
            }
        }
        catch (AggregateException ex)
        {
            ex.Handle(inner =>
            {
                if (inner is Exception)
                {
                    Console.WriteLine(e.Message);
                }
            });
        }
        catch (Exception e)
        {
            Console.WriteLine(e.Message);
        }
    }
}

class D : IAsyncDisposable
{
    public async ValueTask DisposeAsync()
    {
        await Task.Delay(1);
        throw new Exception("I'm inside dispose");
    }
}

https://learn.microsoft.com/ru-ru/dotnet/api/system.aggregateexception?view=netframework-4.8

https://learn.microsoft.com/ru-ru/dotnet/standard/parallel-programming/exception-handling-task-parallel-library

GDI89
  • 54
  • 3