It's a badly crippled implementation that has poisoned the well, the concept is just fine. People like Result<T,E> well enough, and that's exactly as expressive as checked exceptions in languages that implement them correctly.
It's a badly crippled implementation that has poisoned the well, the concept is just fine. People like Result<T,E> well enough, and that's exactly as expressive as checked exceptions in languages that implement them correctly.
1. results must be handled explicitly, exceptions are forwarded automatically. This means I must deal with each result locally, even with a "forward up" to the caller. This means much complex and less readable code
2. exceptions have a stacktrace, handled automatically for you. This means more precise location of where the error occurred. For the same reasons exceptions are heavier - as they keep more data AND is updated for each frame (when handled, and depends on the implementations)
3. the syntax to deal with exceptions is ugly as hell. I haven't found anything better than the python syntax, and still is not something you like to see.
Rust address this very concisely.
In Rust, "forward up if error" is one character, '?', and you can't forget to use it because it also unwraps the non-error value.
In Go on the other hand, "foward up if error" is a notoriously repetitive multi-line sequence. Some people like it because it forces every step of error forwarding to be explicit and verbose. Some people don't.
In Go I've worked with, there's a lot of these. People aren't avoiding error handling to save keystrokes. But occasionally I've found a mistake in Go error-forwarding that went unnoticed for years and would have been detected by Rust-style statically-typed Result, or any kind of exceptions, so I'm not convinced the Go-style explicitness really helps avoid error-handling bugs.
> 2. exceptions have a stacktrace, handled automatically for you. This means more precise location of where the error occurred. For the same reasons exceptions are heavier - as they keep more data AND is updated for each frame (when handled, and depends on the implementations)
That's true in practice, but it's a design choice, not a strictly required difference between exceptions and Result<T,E>.
In principle, the heaviness of a stacktrace can be eliminated in many cases, if the run-time (with compiler help) keeps track of whether an exception's catch handler is never going to use the stacktrace (transitively if the handler will rethrow).
Lazy, on-demand stacktraces are also possible, saved one frame at a time on error-return paths, and sharing prefixes when those have been generated already. These have the same visible behaviour as normal stacktraces, but are much more efficient when many exceptions are thrown and caught in such a way that the run-time can't prove when to omit them in advance.
In principle, the same things can be applied to Result<T,E> types: Take a stacktrace where the Result is constructed, and use the above mechanisms to keep it efficient when the stacktrace isn't used. In Rust, there are library error types that save a stacktrace, if you turn the feature on, but I don't think any of them automate their efficient removal when unneeded, so turning the feature on slows programs unnecessarily.
I don't really understand why the Rust designers didn't feel that adding context to errors would have been a much better built-in macro than just bubbling up the error with 0 info about the path.
What I think I really want is Rust-like with compiler-added return-traces by default, unless explicitly opted out (in code or in build time config), and you can also add extra info if you want.
It's absolutely possible to beat exceptions with manual context, but I've only really seen that in areas of code that were actually causing problems (so people actually put in the work to make the logs useful). When a problem appears in production in an area of code that had not been causing problems in QA is when you're typically left with no useful info in logs.
Not that it fully defangs the issue, but there are two other options that come to mind.
1) suppress the exception (maybe with a log)
2) capture the fact (and maybe content) of the exception and expose it through the API in some other way
Probably neither of these is going to be appropriate for most cases, but seem worth surfacing because 1 is often chosen even when it's inappropriate and 2 is often overlooked as an option.