This problem isn't solved with exceptions either. The problem is that finalizers (C++ destructors, Java's `finally` blocks, Go's `defer` etc.) shouldn't fail but `close()` can fail. Therefore, for 100% correctness, `close()` calls should be handled explicitly and not left to finalizers.
Finalizers shouldn't fail because they might be executed while another exception is already in flight. Three languages have three different behaviors when that happens but in my opinion they all do the wrong thing:
In C++, if you throw from a destructor while another exception is in flight, the program will be terminated. In Java, throwing from a `finally` block will "forget" the original exception. In Go (according to this article, I'm not familiar with it), error from `defer` will be ignored. None of these are ideal.
Go also has `errors.Join` since 1.20, and one of its uses is exactly this one: to be used in deferred close that can possible raise errors so the errors can stack each other: https://stackoverflow.com/a/78013962.
> Doing this automatically is also one of the killer arguments for exceptions over error codes, IMO.
Definitely doing this automatically is better than relying on the programmer to do this manually, but I wouldn't say this is the "killer" argument for exceptions over errors codes, because this doesn't add anything new to the argument of exceptions vs explicit error handling.
Yes, with enough syntactic sugar it can become equivalent. Exceptions can be viewed as sum types with special syntactic sugar in conjunction with the regular return types, and can in principle be implemented as such.
When I say "exceptions", I mean the source-level semantics, not how it's implemented behind the scenes.
That's probably the best option I think. I've heard Ada does that (but don't quote me on that). If you can access the original errors from the `MultipleError` object, at least you can tell the user what exactly went wrong.
I don't thing there's one true right thing™ though. That's why explicit handling is necessary: The compiler doesn't have enough context to handle it for you. The programmer needs to decide what's the right way to handle it.
Personally I do not believe the math of these two monads allows for any better solutions (and I do not believe multierror is correct ;P)... I am thereby also very curious what they think the correct thing to do here is.
Python handles this case by raising the new error but including a reference to the original error. By default, the formatted error shows both:
>>> mylist = []
>>> try:
... first = mylist[0]
... finally:
... inverse_length = 1.0 / len(mylist) # imagine this was something more complex
...
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
IndexError: list index out of range
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "<stdin>", line 4, in <module>
ZeroDivisionError: float division by zero
See my reply to the sibling comment about try-with-resources. I'm not familiar with any of these mechanisms but giving access to suppressed exceptions is probably a step in the right direction.
It might fail, but usually doesn't. In C#, to handle an e.g. file open failure, you can just write
try {
using var file = File.OpenWrite("test.txt");
file.Write("Hello, World!"u8);
}
catch (IOException e) {
Console.WriteLine(e.Message);
}
The exception handler is an enclosing scope for both file open and dispose (flush and close) operations. You can also hoist file variable to an outer scope to, for example, decide what to do if dispose throws for some reason. In practical terms, this is as much of an edge case as it gets, but can be expressed in a fairly straightforward way.
I don't think this does the right thing. You are catching exception if Close fails and nothing else. The problem thought is what to do when after file is opened something fails first, and then Close also fails.
I'm catching any exception that occurs within the scope of try block.
This includes an exception when trying to open or write to the file. If I fail to write to a file, it will try to dispose the stream, which flushes it and closes the file handle. If disposing the file handle itself fails, which should never happen, the exception will occur in the finally block, which this exception handler catches too. If you need to disambiguate and handle each case differently, which is rarely needed, you can order try-catch-finally blocks differently with explicit dispose and different nesting. This, again, is not a practical scenario and most user code will just `using file = File.OpenWrite` it and let the exception bubble up.
Yes, and this ignoring of the original exception is the core of the problem discussed. If you are willing to lose supposedly written data, your approach is golden.
As said in the previous comment, you can place a variable in an outside scope and assign to it from within an try-catch block to handle an open file stream in a particular way upon failure. You can simply write more code to disambiguate and specifically handle errors from separate parts of the execution flow. In any case closing a file handle should never fail, but if it does - there are tools to deal with this.
With exceptions you don’t silently ignore error and go on as if nothing happened.
Just simply not explicitly handling every single possible error is the correct choice in many scenarios - in which case it bubbles up to a general error handler, e.g. telling the user that something bad happened here and here.
My Java knowledge is at least 10 years out of date so I'm not familiar with try-with-resources. But from what I can gather from Google, it looks like they now provide a way to access the suppressed exceptions, which is probably a step in the right direction.
Finalizers shouldn't fail because they might be executed while another exception is already in flight. Three languages have three different behaviors when that happens but in my opinion they all do the wrong thing:
In C++, if you throw from a destructor while another exception is in flight, the program will be terminated. In Java, throwing from a `finally` block will "forget" the original exception. In Go (according to this article, I'm not familiar with it), error from `defer` will be ignored. None of these are ideal.