Obviously sum types for errors are vastly superior to exceptions. Compare the following very readable Error variant pseudocode:<p><pre><code> fn doSomethingFallibly() -> SameVal | Error
SomeVal x = ...;
if failed:
return Error
else
return x
fn doTheThing() -> void
match doSomethingFallibly():
SomeVal x: /* use x */
Error: /* log the error */
</code></pre>
To the utterly unreadable exception-based implementation:<p><pre><code> fn doSomethingFallibly() -> SameVal, except Error
SomeVal x = ...;
if failed:
raise Error
else
return x
fn doTheThing() -> void
try Someval x = doSomethingFallibly()
/* use x */
except Error: /* log the error */
</code></pre>
This is especially egregious if you want to propagate the error. For the error variant case you can use a beautiful monadic solution<p><pre><code> fn doTheThing() -> void|Error
SomeVal x <- doSomethingFallibly()?
</code></pre>
While the exception based variant completely obscures the control flow:<p><pre><code> fn doTheThing() -> void, raises Error
SomeVal x = try doSomethingFallibly()</code></pre>
Obviously exceptions are vastly superior to sum types for errors. Compare the following very readable exception based-pseudocode:<p><pre><code> fn doSomethingFallibly() -> SameVal, except Error
SomeVal x = ...;
if failed:
raise Error
else
return x
fn doTheThing() -> void
try Someval x = doSomethingFallibly()
/* use x */
except Error: /* log the error */
</code></pre>
To the utterly unreadable error variant based implementation that uses obscure functional stuff like patter matching:<p><pre><code> fn doSomethingFallibly() -> SameVal | Error
SomeVal x = ...;
if failed:
return Error
else
return x
fn doTheThing() -> void
match doSomethingFallibly():
SomeVal x: /* use x */
Error: /* log the error */
</code></pre>
This is especially egregious if you want to propagate the error. The exception based variant can focus on the success control flow:<p><pre><code> fn doTheThing() -> void, raises Error
SomeVal x = try doSomethingFallibly()
</code></pre>
While for the error variant case you have to use even more obscure monadic code:<p><pre><code> fn doTheThing() -> void|Error
SomeVal x <- doSomethingFallibly()?</code></pre>
Another take on this is the "Kroll result" after Rachel Kroll of rachelbythebay fame. Basically, it's a C++ type "Result<T>" with methods like "bool isError()" and "T get()" or something like that, but the twist is that it contains a private boolean field "checked" initialized to false, but set as a side-effect of calling isError. If you call get() and checked == false, then it blows up with an exception (I presume it does that too if you call get() and it's actually an error).<p>That way, if you ever write code that doesn't error-check before trying to get the "happy path" result, you'll notice immediately, even if it's the kind of thing that very rarely fails and you haven't written a unit test for it.<p>A chaotic good CS educator might want to try out an experiment: give the students an assignment where they have to work with an API that uses a normal result type, explain why error-checking is really important, but when you're marking the assignment switch out the result type for the Kroll one.
My preference is either a Result-object or the Go convention of (result, error).<p>Both force you to handle the error or explicitly ignore it, which is the correct way. Either you take care of it there if you have the context to figure out if it's relevant or not or you explicitly bump it one step up for the caller to handle.
Not a great article, very little depth, only a shallow mostly syntax-level overview. Notably doesn't at all explore the consequences of those choices in how they relate to convenience, (type) safety, composability, interaction with generics, ... beyond merely mentioning the failure of java's checked exceptions.<p>It's also severely missing in breadth e.g. Swift and Zig use an exception-type <i>syntax</i>, but result-type <i>semantics</i>, it does not cover <i>conditions and restarts</i>, or takes like Common Lisp's MRV, where the ancillary return values are treated as secondary and interacted with through special functions.
It's missing the Erlang/Elixir pattern of returning a tuple `{:ok, T}` or `{:error, E}`, where we can then use pattern matching, or `with` expressions, etc...<p>To be fair, it is very similar to a `Result<T, E>` type, which is why I made this library a while ago: <a href="https://github.com/linkdd/rustic_result">https://github.com/linkdd/rustic_result</a>
An older yet still excellent take on this topic is <a href="https://joeduffyblog.com/2016/02/07/the-error-model/" rel="nofollow">https://joeduffyblog.com/2016/02/07/the-error-model/</a>
I'm writing Go and Dart daily for many years (Go - 9 years, Dart - 4 years). Looking at the Go code (especially old one) I can immediately understand how and where error path is handled. In Dart code it's almost never the case – you just hope that it's handled somewhere in a right place. If I want to really find this place – it's a quest with 10-15 files open. Needless to say I end up inspecting unexpected stacktraces often (rife use of generics also lead to ubiqutuous errors like "type 'Null' is not a subtype of type 'bool'" – that's with Null safety enabled). I think Dart will be introducing even more features and pattern matching and everything-else-that-exists-in-other-languages. Complexity is piling up more and more around error handling in many languages.<p>Go seems to be the only one holding up the ground of caring about cognitive load on developers and code readability.
Missing:<p>* errors from async-colored functions (ie. .then/.catch/.finally and higher order combinators on them, ie. we use a lot of p.catch(log.rescue(...)))<p>* errors in generators<p>* errors in async generators<p>Using functional approaches helps working with things like managing severity, error codes, nested errors etc. that may be attached to errors as well as managing higher level concepts like timeouts, retries etc.<p>This article reads like hello world for errors, there is more to it.
Error handlers, what is called callbacks in the article, have existed since time immaterial in operating systems. They have also been popular in PL/I and Lisp as signals and conditions. Unfortunately, Unix implementation kinda limited them (there is limited number of system signals, error handlers do not stack), so they never became really popular in C.
> For example, "printf" in C can fail, but I have not seen many programs checking its return code!<p>Okay, I add<p><pre><code> if (printf(...) < 0) {
// TODO: Handle error
}
</code></pre>
around the printf call. Now what? How do I handle the error? In most realistic scenarios I can imagine I'd either just ignore it and keep going, or print an error and abort (but what if printing an error fails too? Oh no...), or I'll never actually even get the chance to handle the error because my program will get killed by a SIGPIPE.<p>Seriously, if the printf fails then (barring the malformed format string) that means that the underlying device lost its ability to output data and this ability is most likely <i>not</i> coming back, and your program generally can't do anything with it, or even know about something to do: the functions from the FILE*-family abstract the underlying devices <i>extremely</i> well.
Very timely, was just trying to understand how to improve error handling with typescript recently and came across neverthrow (<a href="https://github.com/supermacro/neverthrow">https://github.com/supermacro/neverthrow</a>) which looks promising…
There's another approach that I don't see discussed often: structure operations so that code just "passes through" on errors and code the happy path.<p>In the case of opening a file, you would get an invalid file handle that you can still pass around to functions and nothing would "fail"; it would just be as if you passed a handle to /dev/null. This scheme requires that you can still explicitly check if the handle is valid.<p>Most people are familir with some form of this with the special NaN floating point value.
The monadic/Result pattern only works if your language has a lot of syntactic sugar for it (for example to mix functions that can return errors with ones that can't you need some kind of 'bind'), but when you're used to it and don't try and play too many clever tricks on the side (not _everything_ has to be a monad) then it can be very readable and easy to work with.
Nice article, and I'm not just saying that because I wrote something extremely similar myself a few years ago. ;) The one thing I'd add is that the "defer" construct deserves a mention. It's not a comprehensive error handling mechanism by itself, but can often augment or even stand in for one.
Great that we have more discussions about error handling.<p>Shameless Plug: "Musings about error handling mechanisms in programming languages"<p><a href="https://www.amazingcto.com/best-way-to-handle-errors-for-a-programming-language/" rel="nofollow">https://www.amazingcto.com/best-way-to-handle-errors-for-a-p...</a>
For those who would rather use an Either type instead of returning 2 variables in Go, I made this: <a href="https://github.com/samber/mo">https://github.com/samber/mo</a>