Proper way to deal with exceptions in DisposeAsync
Asked Answered
A

4

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.

Aggri answered 20/11, 2019 at 12:8 Comment(12)
"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.Sonia
@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.Aggri
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.Sonia
@canton7: Well, in a language-with-exceptions every statement might be an early return :-\Aggri
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.Sonia
@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.Aggri
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.Sonia
This is a design question concerning a feature of C# 8. IMO, the discussion better belongs to GitHub repo here. P.S.: I completely agree with your concern, the work-around, and how it is supposed to work.Dictatorial
@TanveerBadar: As far as I remember from silent reading the language design repo, it’s more the place for proposals than for clarification requests.Aggri
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.Butyrate
@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?Aggri
Related: Should you implement IDisposable.Dispose() so that it never throws?Barre
C
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;
}
Chophouse answered 4/12, 2019 at 22:40 Comment(2)
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).)Aggri
@Aggri 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.Chophouse
G
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
    }
}
Goff answered 20/11, 2019 at 13:36 Comment(9)
Btw, source.dot.net (.NET Core) / referencesource.microsoft.com (.NET Framework) is a lot easier to browse than GitHubSonia
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?Aggri
@Sonia Awesome! I was aware of referencesource.microsoft.com, but didn't know there was an equivalent for .NET Core. Thanks!Goff
@Aggri "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.Goff
`Dispose* should never throw. From the docs: "To help ensure that resources are always cleaned up appropriately, a Dispose method should be callable multiple times without throwing an exception."Inquisitor
@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.Goff
@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.Inquisitor
@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, 2.Aggri
@vlad, not what I said. And the same should happen in Dispose - program termination.Inquisitor
C
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.

Cyclops answered 2/12, 2019 at 17:18 Comment(12)
So your proposal effectively boils down to suppressing exceptions on connection close, right?Aggri
@Aggri 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.Cyclops
@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." learn.microsoft.com/en-us/dotnet/standard/garbage-collection/…Cyclops
@Aggri 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.Cyclops
@Aggri 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.Cyclops
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.Aggri
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?Aggri
@Aggri 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"?Cyclops
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.Aggri
@Aggri "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.Cyclops
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.) 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.Aggri
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.Aggri
I
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

Interrelated answered 5/12, 2019 at 9:1 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.