The benefit of exceptions and try/catch is that it lets you separate your exception handling logic from the mainline logic of the function. Having error logic weaved in and out of mainline logic just obscures what is going on and increases cognitive load. I much prefer writing and reading code with exceptions than explicit error handling control flow.
> it lets you separate your exception handling logic from the mainline logic of the function
So do the error monads (on Rust or Haskell). In fact, they offer a lot more flexibility on how to separate them, and can put even more distance between the happy path and error handling (if you need it, often people use the extra flexibility to place them closer).
You mean, handling all the corner cases and making sure your code is correct?
There are moments when the experience of programing in Haskell feels like "pleasing the type system". Those are few and far in between, and most times they are still because of a bad error message that converted your bug into a complex type error.
If your experience on the language gets those all the time, I suggest you get some more experience, because you are clearly still unable to design your types very well.
> If your experience on the language gets those all the time, I suggest you get some more experience, because you are clearly still unable to design your types very well.
It's possible to advocate for Haskell without casting aspersions on someone's abilities.
Sorry, it gets tiresome to hear the "tried Haskell once, types are a huge problem" claim again and again.
The language does have a steep learning curve, and it's no personal flaw to not get it even for a long while. But to go on and complain that the language is badly done because they couldn't get it on the first few tries is a deeply annoying display of hubris.
> I much prefer writing and reading code with exceptions than explicit error handling control flow.
Can you let us know what languages that use "explicit error handling control flow" you have used?
I've extensive experience with both an much prefer the "explicit error handling control flow" in Rust/Haskell/Elm/Kotlin/ReScript, than the exceptions in Java/C++/C#/JS/Ruby/Python.
Interesting that the use of implicit nulls (another of my annoyances in langs) is also split along these lines!
I've extensive experience with both an much prefer the "explicit error handling control flow" in Rust/Haskell/Elm/Kotlin/ReScript
How explicit are those languages really, though?
Take Haskell, for example. It’s common to indicate potential failures by having a function return Either/Maybe. Those are monadic, with their join behaviour propagating the Left/Nothing result that conventionally represents the failure case(s). It’s idiomatic to write a function that calls a series of potentially failing functions and not examine the result of each call immediately. Instead, you defer any handling of failures to the end of the chain or even pass the result back to the calling function via another Either/Maybe.
What you’re not doing in any of those alternatives is explicitly checking the return value after each function call and handling any failure immediately right in the middle of your default execution path. So is this really much more explicit than exceptions? The possible failure modes for each function are encoded in its type, but there are implementations of exceptions that also have that property, so that’s not really a point in favour of either side. The code calling the functions is still highlighting the default execution path and shifting the recovery from any other cases elsewhere.
As another example, Rust follows an analogous convention with functions returning Option/Result to indicate potential failure modes, but has try! and then added the ? operator to propagate errors with minimal extra code instead of writing all the boilerplate manually each time. This is arguably more explicit when calling those functions, since at least any function that can fail needs to be called with ? (or something more obvious) to handle the result, but fundamentally the convention is still trying to minimise clutter in the default execution path and delegate error handling responsibilities to code somewhere else, and it still doesn’t indicate anything more explicit about what the potential failure modes for each function are at the place where that function is called.
> Instead, you defer any handling of failures to the end of the chain
That's a choice. You can also do it immediately.
Also, no warped control flow: just regular "call-return" flows.
> What you’re not doing in any of those alternatives is explicitly checking the return value after each function call and handling any failure immediately right in the middle of your default execution path.
Like said, that's a choice. You can setup monadic handling, you can do each by itself. It depends on the situation what you use: but the DEFAULT is each by itself.
> As another example, Rust
Still the control flow remains intact. While Exceptions mess with control flow.
The separation between the mainline logic and the exception handling logic does not require an exception mechanism like in Java or C++.
A restricted form of GOTO, like in the language Mesa (from Xerox) is good enough.
Mesa also had exceptions for things where exceptions are appropriate, e.g. numeric overflow, out-of-bounds access or memory allocation errors, i.e. errors that are normally caused by program bugs and which can be solved only by someone who knows the internals of the program.
For errors whose cause can be determined and removed by the user, e.g. files that cannot be found or mistyped user input, the appropriate place of handling the error is the place where the function that failed had been invoked.
Neither inside the function that failed nor several levels above the place where the error has been detected it is possible to provide really informative error messages that can enable corrective action, because only at the place of invocation the precise reason is known why the failed function had been called.
The restricted GOTO from Mesa, whose purpose was error handling, could not jump backwards and it could not enter a block, which eliminated the possible abuses of the feature.
Moreover the labelled targets of the GOTO could exist only inside a delimited section at the end of a block.
The keyword GOTO is not needed, because it is redundant. At the place of the jump it is enough to write the target label, as no other action is possible with it.
So in a language like Mesa, the mainline logic would be something like (in languages with "then", no parentheses are needed, unlike in C and its followers):
if err_code := function_that_can_fail(...) then Error_Name
if err_code := function2_that_can_fail(...) then Error2_Name
and the error handlers will be grouped in a section similar with the error handlers used with the exception mechanism of Java or C++.
The difference is that the section with the error handlers must be in the same file with the function invocations that can return errors and the error handlers will be invoked only from there, not from random places inside who knows what 3rd party library might have been used somewhere.
Because for such handlers there is no COME-FROM problem, you know exactly what has happened and you can easily determine what must be done.
The challenge with exceptions is there is zero indication at the call site that a function can throw. Does myFunc() throw? Only way to know is to dig down through the entire call stack. Meanwhile with (value, error)/Result etc. it's obvious right at the call site whether a function can potentially error or not.
It's the exact opposite
With the either functional approach you just move the error around, and when you can't deal with it anymore you don't even have the stacktrace to know what damn piece of code created the error in the first place
I really don't understand the hate for checked exceptions either. The only downside to me is that you're forced to deal with exceptions when trying to prototype some code. A compiler option to ignore checked exceptions would easily fix that.
If you want to use exceptions then checked exceptions or the equivalent seems like a more logical approach than allowing anything to throw anything at any time. Which exceptions a function can raise is a part of its interface, so it might as well be documented and/or automatically recognised as such.
However, this style probably requires both a carefully considered set of possible exception types and good tool support to work well for developers in practice. Clearly we’d like to have both of those things in any case, but I’m not sure the average Java programmer at the peak of the checked exception debate had either, and it’s almost always in the context of Java that I see checked exceptions being criticised.
If you are dealing with functions that can end up propagating many different types of exception under sufficiently unusual conditions and you don’t have easy ways to do things like inferring exception specs for higher-level functions calling them or separating or consolidating multiple exception types as needed when handling them, that seems like a recipe for frustration. It’s easy to imagine writing endless boilerplate lists of obscure exception types for every function and then having to maintain those across your whole codebase any time some widely-used low-level function changes its spec, which seems completely impractical to manage at scale without good language and tool support.
They do cause issues when you need to implement an interface which doesn't declare the exception but you have a checked exception which can't be handled at that point. Only real option then is to rethrow as an unchecked exception. For example an Iterator which may have an IOexception.
I still like them, but they aren't a panacea, at least as Java implements them.
Yup. I would love to have checked exceptions in C++. They seem like the best possibility, at least for a language that can't support the monadic pattern thoroughly.