> Some errors are unexpected and should stop the program; you want to use exceptions for those.
Precisely the opposite: exceptions are a fail-fast mechanism that gives you an alternative to terminating the program.
Now, as slx26 mentioned, it's only half of the story. Most APIs (including .NET) document exceptions badly, they're not discoverable, and if you try to use them to _recover_ from a condition, you're in for a world of pain. The workflow is usually: attach the debugger, try to make the exceptional condition happen, inspect relevant info in the debugger and write the catch block.
I wish that programming languages supported some contract-like mechanism of declaring: "This method can throw only X, Y and Z". If the method throws anything else, a system-defined "UnexpectedException" would be thrown, encapsulating the invalid one. C++ used this model once upon a time, but they went away from it due to runtime costs and it being little-used. (Also, it terminated the program instead of rewrapping the exception.)
Exceptions are first-class values, but few programmers treat them as such, probably because the programming language allows them to do so. So we should start by fixing PLs.
There's no need for even that complexity. If base exception type had a single property, something like "CanRetry", all exception handling would be simple.
Because when it comes to exceptions there are really only 2 things you can do: abort the current operation or retry it.
The code generating the exception will know which of these is appropriate and the try/catch handler is what would restart or cancel the operation.
The exact details of the exception are for debugging not for program flow.
> If base exception type had a single property, something like "CanRetry",
"Can retry" _WHAT_? This can work if you meticulously rewrap low-level exceptions into higher-level ones that reflect the high-level operation that failed.
Concrete example: FileNotFoundException. I'd say that in "normal" circumstances it's not retriable: you're looking for a file, it's not there, so an exception is thrown. In "unusual" circumstances you're polling (i.e., waiting for a file to appear somehow) or you're an OS shell and is searching the path for the location of the program.
Where you handle exceptions has nothing to do with where they are thrown. You put your try/catch around whatever operation can be retried at the top level. I don't know why you'd need to rewrap exceptions -- I've never done that.
In that handler, you just need to know if the exception is fatal or a temporary issue for which retrying might succeed. If there was a CanRetry flag on the exception, that makes that determination easy without having to know every potential exception type.
Your FileNotFoundException is a good counter-example as maybe the called code is unable to know the intent. So, yes, in that case the handler has to make this determination based on the type (as we do now) or some code in the middle, that knows the intent, needs to catch and set that flag.
But most of the time one can determine at the point exception is thrown whether or not it's a potentially temporary situation (like a network error) or a unexpected fatal program logic error.
> Where you handle exceptions has nothing to do with where they are thrown.
Funny you say this, when it's demonstrably NOT the case: a throwing method NOT wrapped in a try/catch will NOT have its exceptions handled at the call site. And vice-versa. If you want to retry a particular failing operation, you write try/catch around IT, not several levels up the stack.
You _could_ write it several levels up the stack IF you have precisely typed exceptions, therefore wrapping.
> I don't know why you'd need to rewrap exceptions -- I've never done that. [...] maybe the called code is unable to know the intent
To convey meaningful semantic information about the (business) operation that failed. Updating a record in the database can fail due to business rules (DB constraints), network connection that disappeared, concurrency conflict, transaction deadlock, etc. The user or higher-level code is not interested in the root cause, but in the actual consequence ("Could not update record". And yes, "IsRecoverable" flag, the value of which depends on the inner exception and its properties.).
And yes, the called code rarely knows the intent. There are a bunch of libraries out there being used in diverse contexts. So you catch and wrap the exception. Wrapping wouldn't be needed if library authors were careful about designing their exceptions, but I've rarely seen this to be the case. Even C# guidelines recommend you to use the generic, system-provided exceptions if an "appropriate one" exists. (IMO, a most terrible advice. And I discovered it was terrible by first following it then going back and designing "proper" exceptions for the system.)
> Your FileNotFoundException [...] needs to catch and set that flag.
But the exception type does not have that flag. So you have to wrap it in another exception. (Though all exceptions in C# have a Data field that is object -> object dictionary accessible to anyone. So you could use that.)
> You put your try/catch around whatever operation can be retried at the top level.
What is "top-level" for you? The shell's REPL loop? Exception blocks are non-restartable, so how would REPL continue the path-searching loop that threw FileNotFoundException?
> temporary situation (like a network error)
Ah yes, I love these. Someone pulled the power cable on some router the computer is indirectly connected to. To the program it looks the same as ordinary timeout error. How temporary is it?
> If you want to retry a particular failing operation, you write try/catch around IT, not several levels up the stack.
Generally speaking when I retry and operation it's pretty far up the stack that I restart it. I'm not retrying sending a single byte, I'm retrying the entire file transfer operation (as an example). If it's batch job processing data in a loop, then the processing of each item is typically where I would catch and retry or ignore. If the exception actually said "I think you should retry" then it could retry otherwise it would abort.
> To convey meaningful semantic information about the (business) operation that failed.
Wrapped exceptions tend to provide less information than the root exception. I agree that library authors aren't as careful as they could be about exceptions and you might need to wrap an exception just to make it sane. Generally most libraries throw LibraryException and the real exception, with meaningful information you can action, is in the inner exception. I blame Java's checked exceptions for making that a thing.
> But the exception type does not have that flag.
Right. That's why I proposed it. "If base exception type had a single property... something like "CanRetry"... all exception handling would be simple."
> To the program it looks the same as ordinary timeout error. How temporary is it?
Never forget to put a limit of retries. You could get really clever and put an exponential delay on it.
> I wish that programming languages supported some contract-like mechanism of declaring: "This method can throw only X, Y and Z". If the method throws anything else, a system-defined "UnexpectedException" would be thrown, encapsulating the invalid one
Boy, do I have some news for you... Like about 25 years old news. Did you ever try java?
I did in the original post, and that's why I used C++ as example instead of Java. A method declares `throws X, Y, Z` and _runtime_ checks that no other exception escapes. No source changes needed if you add W to the list. And if some other exception escapes, it's wrapped in `UnexpectedException` that is reserved for and throwable only by the runtime.
I guess we could extend Project Lombok to do this. You'll still have your "throws" statement but then the tool would wrap calls to catch those that are not listed in "throws" statement and wrap them as you say.
It just occurred to me that I could probably do the same with attributes and DynamicProxy for C#. And also implement additional checks like "IF X is thrown, its properties must satisfiy some constraints.". (I program both in Java and C# these days.)
Such "UnexpectedException" as I suggested would serve two purposes: 1) well, knowing that something unexpected happened and allowing you to handle it with "last chance handler", 2) helping the developers maintain the contract. If you change a method so that it can throw some new exceptions (compared to the previous version), you've broken its contract/compatibility. This would then show up during testing.
> You'll still have your "throws" statement but then the tool would wrap calls t
Yes. Fortunately, Java allows you to mention subclasses of RuntimeException in "throws" declaration.
But... both purposes 1) and 2) are already served perfectly fine with the current exception models already: you catch Exception at the very top-level in the "last chance handler", and if during testing an unexpected exception is thrown, it bubbles up past the existing handlers right into this "last chance handler". What does re-wrapping help with, exactly?
Unless the method 1) throws an exception which it should not have according to its declarative contract (annotations in Java, attributes in C#), and 2) it gets (erroneously) handled by an intermediate handler.
> What does re-wrapping help with, exactly?
It makes it clear that the method broke its declarative contract, which is what exceptions are _for_. And given the restriction that only the runtime can throw such exceptions, you're sure that no other method can randomly throw such exceptions because... they like it so.
Well, "(erroneously) handled by an intermediate handler" is a tricky situation: would it be really handled incorrectly?
Another question is ergonomics. It's trivial (but tedious) to write code like this:
class Foo {
// ...
public void Frob(...) throws FooException {
try {
// ...
}
catch (Exception e) {
throw new FooException(e);
}
}
public void Blarg(...) throws FooException {
try {
// ...
}
catch (Exception e) {
throw new FooException(e);
}
}
}
which is actually a "best practice" already ("annotate inner exceptions with some high-level context and wrap them in high-level exceptions") — and adhering to it makes your proposition completely extraneous, because nothing can throw an UnknownException ever.
And in before "don't catch and wrap Exception!", consider that Foo maybe parameterized by some dependency that may be implemented as a network service, or a disk file, or a DB: three different implementations will throw completely different exceptions: FileNotFound vs NetworkConnectionClosed vs OdbcInvalidManufaturer. Either dependency interface allows implementations to throw any of those (so that the user of Foo, who knows which implementation it specified, can handle those), or it makes them to wrap them all into DepException, but then, again, this means that Foo's methods will catch-wrap-rethrow DepExceptions instead of just Exceptions.
If previously only DoB could throw SomethingEx and now DoA() can also throw it, the handler is almost certainly incorrect handling if this is some "generic" exception. C#/.NET base library is notorious for using InvalidOperationException for all kinds of unrelated crap.
> It's trivial (but tedious) to write code like this
Eh. Methods without "throws" would be "unchecked" at runtime as well. So my (imagined) best practice would be that non-private methods declare their exceptions and leave it to the caller whether and how to wrap them.
> And in before "don't catch and wrap Exception!"
Oh, I do that all the time for precisely the reasons you mentioned. Exceptions from the lowest level are most precise and least useful as there's no information about the context. (Unless you go down the unmaintainable rabbit hole of parsing the call stack.)
So, if I understand your proposal correctly, you want to split the current 2-pronged approach "catch everything, handle what you can, re-throw what you can't wrapped in FooException" into a 3-pronged one: "catch what you believe can be handled by you or your users, handle what you can, re-throw what you can't wrapped in FooException, and the system will re-throw everything else wrapped in UnexpectedException". The difference is that if now some dependency of Foo would change drastically, the Foo's users won't be able to accidentally swallow or even see the new exceptions, those will go all the way up as UnexpectedExceptions and will draw the due attention.
> if now some dependency of Foo would change drastically
The most frequent "drastic change" being writing new code and fixing bugs. It's extremely easy to widen the set of exceptions thrown by a method and zero tooling to help you with finding out what exceptions can be thrown from the code.
Say what you want about Java, but its division of throwables into errors and exceptions makes sense. Errors like stack overflow, VM faults, etc., should not be exceptions. Under this scheme, they would always propagate out of the method unwrapped. Again, .NETs predefined exception types are botched beyond repair.
"> Some errors are unexpected and should stop the program; you want to use exceptions for those.
Precisely the opposite: exceptions are a fail-fast mechanism that gives you an alternative to terminating the program."
I don't see how this logic flows.
Exceptions do give you the alternative to recover, sure, but the author is saying 'the kinds of errors that produce states where you should stop the program ... use exceptions'.
You're not really disagreeing it seems.
Where you might disagree, is that the author is indicating the 'recovery cases' are more suited to being straight error returns while you're hinting at exception recovery.
The author wrote "stop the program". I took it to mean literally: stop the program, i.e., exit immediately, i.e., crash. That's not acceptable in long-running "service" programs.
Precisely the opposite: exceptions are a fail-fast mechanism that gives you an alternative to terminating the program.
Now, as slx26 mentioned, it's only half of the story. Most APIs (including .NET) document exceptions badly, they're not discoverable, and if you try to use them to _recover_ from a condition, you're in for a world of pain. The workflow is usually: attach the debugger, try to make the exceptional condition happen, inspect relevant info in the debugger and write the catch block.
I wish that programming languages supported some contract-like mechanism of declaring: "This method can throw only X, Y and Z". If the method throws anything else, a system-defined "UnexpectedException" would be thrown, encapsulating the invalid one. C++ used this model once upon a time, but they went away from it due to runtime costs and it being little-used. (Also, it terminated the program instead of rewrapping the exception.)
Exceptions are first-class values, but few programmers treat them as such, probably because the programming language allows them to do so. So we should start by fixing PLs.