Personally I follow with the trend and feel strongly that the Result-type approach is superior over (unchecked) exceptions in most cases, since: (1) they are much easier to reason about for me as a programmer reading and writing code, as it (2) produces code with easier execution flow and (3) uses the type system to specifically+exhaustively communicate the possible outcomes.
I say "in most cases", as I believe there are (probably) some behaviours that are easier to express using exceptions, but in practice I rarely run into such cases. And when I run into them, I might simply not be able to come up with the right abstraction using Result-types.
I find the Rust/Kotlin trend of only allowing one exception (the panic), as to dictate the language users to use other means to communicate/express multiple outcomes of a call. Both languages --- by having sum types + pattern matching switch statements + exhaustivity checks on switches over sum types --- also provide means to express/handle multiple outcomes of a call, thus rendering the prevalent use of exceptions mostly irrelevant.
The hierarchy of exceptions that you mention are to me something that goes against the nature of typing discipline: they subvert the typing system (in case of unchecked) or add a additional typing component (in case of checked) to express/handle multiple outcomes of a call.
I would hate to have to reason about the 250 possible errors coming from every single library underneath my app, especially when the immense majority of those are better left to be solved by the user (e.g. file cannot be opened because the usb stick was removed, audio device unavailable because you are using a pro audio driver that locks to a single app, the GPU cannot be opened because I did a driver update and must reboot, etc etc).
Monadic error handling is fine when you have a finite, predetermined set of "things that can go wrong" - e.g. you're writing a small input parser for a text field, either it parses or it does not. But imho this is a microscopic amount of cases compared to all the "random stuff that can go wrong" in the average GUI app. And as a GUI app user, I really really hate when the app tries to do what's best for me - autoreconnect, write to another file, change the settings automatically, whatever. Just gimme an error popup and let me decide.
> produces code with easier execution flow
In C++ using RAII objects for state makes that concern pretty much disappear imho. Save your state & rollback in a tiny type's ctor/dtor and you're good to go, whatever you do, things can throw and what you were doing will be rolled back. Not as good as hardware transactional memory, sure, but apparently not a lot of CPU engineers are able to make that one work yet...
> (3) uses the type system to specifically+exhaustively communicate the possible outcomes.
back to 1, this is what I really do not want. Just write the happy / known-to-the-programmer path and let what you don't know about bubble up to main() or the top of your event loop.
That's is why I think exception are bad: the programmer usually ends up not thinking about the errors that can occur (and they often cannot even do it if they wanted because exceptions can come from anywhere) and just propagates to the user, who often doesn't even have access to the documentation of the underlying library, and must perform Google search on some cryptic error message, just to discover that the program is failing because some log formatting library he doesn't care about cannot have access to the configuration file it expected. This is a terrible user eperience.
And yes, checking 250 different kinds of error can be tedious (but it should never be so high anyway), but at least with sum types it's doable because the type system is here to help you cover every case. And then you can manually check which error is a reasonable one to return to your user and which is a recoverable one that should need no external intervention.
If the USB drive has been pulled out too quickly, just tell it to the user instead of returning a “aborted: did not have the rights to write to the file” exception.
you know what is even worse ? the programmer "handling" the error and just displaying a "we're sorry, an error has occured" message to the user who now has exactly zero way to know what is going wrong. Or as seems to be common in Rust, Go, and other exception-free systems, just calling panic! ... yay, "error handled".
It is very irrealistic and presumptuous to assume that your program has enough domain knowledge about its users's systems to handle every single error.
This occurs a lot with language with exceptions too: you need to “handle” the exception before it reaches the root, otherwise it crashes the whole software (which is basically the same behavior than a panic in Rust or Go), so people usually add this kind of catch-all exception handling in Java or any language with exceptions. Have you ever encountered a “500 internal server error” while browsing so website? That's it.
The main difference is that you can't do any better when using exception because you can't easily list all possible exceptions that can reach the root, while you can in languages with sum type.
Of course there will be lazy programmers in any language, the problem with exception is that even if you aren't lazy, you cannot really do better than people who are (unless you can disable exceptions, like in C++ …).
If you're writing an application and don't really care about the error types, just use `anyhow::Result` (or typedef your own `Result<T, Box<dyn Error>>` to avoid a dependency) as your error type and bubble everything up to main.
Then if there are cases where you want to handle specific errors… you can still do that, easily, with the help of the compiler.
Not really.
> for essentially the same result
With the huge differences that
1. it signals that things can fail, which largely doesn't exist with unchecked exceptions unless you re-add that information which is going to lead to similar amounts of "characters added" (e.g. “throws”)
2. you're explicitly opting into the costs and information-discarding effects, which is rather important for the language's purported goals, it supports that use-case without discarding its other important use-cases
> exceptions were introduced because bubbling things up explicitely is a pain !
Not really either, exceptions were introduced to structure error handling rather than use ad-hoc and generally in-band methods, or unrestricted jumps. They also provided a very useful hook point to provide contextual information (by default or otherwise) which the aforementioned ad-hoc methods didn't account for e.g. not much contextual data you can bundle in a shell or C error code.
Leveraging sum types for error handling is an oddly recent development, both because most languages don't have sum types, and because that it could be used thus doesn't seem like a very old realisation at large (just look at error handling in Haskell).
> Just write the happy / known-to-the-programmer path and let what you don't know about bubble up to main() or the top of your event loop.
I believe that is the point, an IO exception o some sort is a known exception case so the return type of the function should express that. This function can return either the contents of a file or an IOError if the file cannot be read for some reason. The caller can then decide whether they want to try and handle the exception case or ignore it and let it bubble up the stack, but the function should clearly express that it is a possible outcome.
Depends on the ergonomics - if it's easy to convert errors to panics then you get that in application code while also explicitly enumerating error cases as an API author.
I haven't used Rust enough seriously to comment authoritatively on this but it seems like a good trade off - I hate it when the library I'm using doesn't document error cases
With this approach, error handling is pretty minimal. All I have to think about is exception-safety. And that's an easy thing to reason about, when language does not do manual memory management.
I understand that this approach is not suitable for every software field. If I'm writing martian rover software, I don't want to crash my software, when rover might be physically destroyed.
Of course some exceptions are not exceptional and they are part of normal flow. But those situations are rare and try-catch is perfectly fine to handle those errors as well. I'm not forced to handle them, that's right. But even if I did not think about it, I'll get crash, I'll get logs, I'll get user complaints and I'll implement that separate handling. Probably it's corner-case anyway, so that's not a big deal.
And a big plus is that I don't have to think about exceptional situations when I'm writing my code. That reduces cognitive load and allows me to focus on main logic, rather than thinking about every possible outcome of every called function. And main logic is why I'm writing a software in the first place.
Unchecked exceptions are untyped indeed. They are like JavaScript on top of typed language. Well, I'm not strict-type purist. I'd use Haskell if I were one, I guess. Dynamic typing is popular for a reason. May be it would be an interesting project to implement strictly-typed exception handling. But Java did it wrong. I don't know how to make it right. If someone would make strictly-typed exception handling natural and friction-less to use, I'm all for that. But I did not see that kind of a system anywhere. And untyped works for me good enough.
In many applications errors are dispatched to some common handler far above the call stack. This is also the justification for uncheked exceptions in C# given by Anders Hejlsberg.
Not providing any value judgement but… this seems completely irrelevant to the topic. Rust has long-settled on not exceptions, and they are largely incompatible with the language's stated goals and purpose.
> Rust's panics are similar to exceptions
Except for the part where their very existence is optional.
> so for me the ideal implementation should include some short syntax to allow converting from Result to result value and throw panic for errors
You mean like `Result::unwrap` which has existed since before Rust 1.0 was a thing?
> but also allow hierarchy of panics and try-catch handling of those panics (so it would throw some specific panic, not just common panic).
I don't want to be too forward about it, but that's a language which really has nothing whatsoever to do with Rust: the "hierarchy of panics" alone is completely disqualifying so… I see even less what that comment's doing in here.
I think that it's possible to build a bridge from Result to panics, thus allowing a developer to choose a particular style of error-handling. I don't understand why exceptions are incompatible with language stated goals.
> Except for the part where their very existence is optional.
But that's up to developer to decide, right? By default panics do work. If I want to call `abort()` on panic, I have to explicitly configure it.
> You mean like `Result::unwrap` which has existed since before Rust 1.0 was a thing?
Yes, but with improved panics (including hierarchy of panics, capturing stacktrace) and enhanced language syntax to better integrate that model into a language, rather than discouraging people from using it.
Because they require significant runtime support.
> But that's up to developer to decide, right? By default panics do work. If I want to call `abort()` on panic, I have to explicitly configure it.
Not necessarily. It's the user of a library which decides whether they allow panics or not, so libraries can not assume panics will be available at all.
> Yes, but with improved panics (including hierarchy of panics, capturing stacktrace)
panics already capture stacktraces (if you run with the proper envvar enabled). As to hierarchies of panics… how do you propose to do that in a language without inheritance? Adding inheritance?
> and enhanced language syntax to better integrate that model into a language, rather than discouraging people from using it.
But the language should discourage people from using it. Having multiple ways of doing errors is actively bad, especially when one of them is completely unnecessary and plain doesn't work in many environment.
Yes and no. If you need either semantic, you need to specify it, so that when others use your code, they ensure they don't get something that's broken. They "work by default" in the sense that that's the default behavior, but people don't rely on it because unless you say you require it, they may use your code without them.
panic=abort is also a somewhat commonly suggested way of slimming down binaries.
Completely agree here.
What we dislike is rust fanboys spreading misinformation about GC, misinformation about exceptions, misinformation about mutability..
As far as I know rust implements that with the '?' syntax.
match foo {
Ok(v) => v,
Err(e) => return From::from(e)
}
What "converts a Result into a value and panics on error" is Result::unwrap[0], or Result::expect[1] (for a custom panic message).[-1] as of rustc 1.46 anyway, I don't know how "try blocks" will effect `?`, though possibly the combination of "try blocks" and unwrap would make it easier to just panic an entire function: currently each Result site has to be unwrapped by hand whereas otherwise you could just `?` every site normally and unwrap the entire block I guess?
[0] https://doc.rust-lang.org/std/result/enum.Result.html#method...
[1] https://doc.rust-lang.org/std/result/enum.Result.html#method...