Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

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.



Java with try-with-resources does the correct thing: It attaches the new exception as a secondary exception to the currently in-flight exception.

Since function calls form a tree, exceptions must form a tree as well.

Doing this automatically is also one of the killer arguments for exceptions over error codes, IMO.


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.


How automatic it is depends on the language, but error "objects" (rather than codes) can do this too. It is pretty great.


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.


> in my opinion they all do the wrong thing

What would be the right thing? Combining the original exception and the error from `close` into some kind of `MultipleError`?


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


No. Your example is not similar to what we are discussing here

    class A:
        def __del__(self):
            1/0
There is no way to catch that exception. It will just print a warning with the exception but it can't be handled.


Java also does this.


The problem is handled with exceptions plus Java's try-with-resources or C#'s using statements or Python's context managers though, right?

Furthermore in Java since version 7 you can actually see both exceptions with the suppressed exceptions pattern.


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.


Well the point it: the problem is not as simple in C# as your initial snippet suggests.


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.


What about Java's try-with-resources?


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.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: