
Views on Error Handling - dannas
https://dannas.name/error-handling
======
enriquto
The very concept of "error handling" is absurd.

There are no errors, just unnecessary abstractions and control flow hacks. You
try to open a file; either you can or you cannot, and both possibilities are
equally likely and must be handled by normal control flow in your program.
Forcing an artificial asymmetry in the treatment of both cases (as championed
by the error handling people) adds ugly complexity to any language that tries
to do so.

~~~
dllthomas
Can you name a language/ecosystem that gets it right?

~~~
dinosaurdynasty
Rust seems pretty close to this (with Result<T, E>), though there is also the
panic system.

~~~
platz
it's trivial in any language to define and use a Result<T, E> type.

~~~
saagarjha
No, not really. Languages that don’t really embrace such a type can usually
never make its use ergonomic. Adding such a construct to C or Java would be
“trivial” but its use would be exceedingly painful.

~~~
The_rationalist
Here's an example of a result type in Kotlin implemented as a library monad.
(the language is getting an official one but is currently less featureful than
this lib)

You can use exception or Result depending on when it makes the most sense and
this is the best of both worlds.
[https://github.com/kittinunf/Result](https://github.com/kittinunf/Result)

~~~
saagarjha
Swift had a bunch for a while too, but it’s getting an official one as well.
Again, that’s only because the language provides the right tools for it to
exist, in languages where you don’t have this you’re going to have a very hard
time trying to add these things in.

------
phoe-krk
There seems to be no mention of the Common Lisp condition system, which allows
for handling of exceptional situations actually done right. Is this omission
purposeful?

See
[https://news.ycombinator.com/item?id=23843525](https://news.ycombinator.com/item?id=23843525)
for a recent long discussion about my upcoming book on the topic. (Disclosure:
this is a shameless plug.)

~~~
dannas
I've collected references to error handling but - I have to shamefully admit -
have never encountered Common Lisp's condition system.

I'll take the time to read up on it properly, but from a quick glance it seems
to me to be in the category of non-local transfer of control with a co-routine
flavour.

It looks powerful, but I get the sense that a lot of language designers are on
purpose trying to restrict the powers of error handling. So returning sym
types or error codes are simpler than throwing exceptions which - again looks
to me - to be simpler than allowing transfer of control to be decided at run
time as in the condition system.

Again, very interesting. And thank you for making me aware of its existence.

~~~
phoe-krk
> with a co-routine flavour.

Kind-of-but-not-exactly. There are no coroutines whatsoever; the main
technical defining point is that the stack is not unwound when the error
happens, but it is wound further. Some code is executed that then searches the
dynamic environment for matching error handlers, which are executed
sequentially; these are then capable of executing arbitrary code provided from
earlier in the stack in form of invoking so-called restarts; both handlers and
restarts are also capable of performing transfers of control to any point on
the stack that was properly annotated as being available for such.

------
gumby
> but without the downsides of the costly C++ memory deallocation on stack
> unwinding.

I.e. I don’t care about restoring the program to a known state when handling
an error (memory deallocation is just one case of processing unwind blocks;
locks need releasing,my file handles returned to kernel etc). This really only
makes sense when your error “handling” is merely printing a user friendly
error message and exiting.

~~~
dannas
More context to that quote: >Per Vognsen discusses how to do course-grained
error handling in C using setjmp/longjmp. The use case there were for arena
allocations and deeply nested recursive parsers. It’s very similar to how C++
does exception handling, but without the downsides of the costly C++ memory
deallocation on stack unwinding.

I have never used setjmp/longjmp myself. And I agree with you that my first
instinct would be to use it in the similar manner as in many GUI programs:
they have have a catch statement in the message loop that shows a dialog box
of the thrown exception. You just jump to a point where you print a user
friendly error message and exit.

But I still can imagine use cases where you've isolated all other side effects
(locks, shared memory, open file handles) and are just dealing with a buffer
that you parse. Has anyone used setjmp/longjmp for that around here?

Given your many years in the field and Cygnus background I guess you've used
it a few times? Do you happen to have any horror stories related to it? :-)

~~~
gumby
I hate setjmp/longjmp and have never needed it in production code.

Think about how it works: it copies the CPU state (basically the registers:
program counter, stack pointer, etc). When you longjmp back the CPU is set
back to the call state, but any side effects in memory etc are unchanged. You
go back in time yet the consequences of prior execution are still lying around
and need to be cleaned up. It's as if you woke up, drove to work, then
longjmped yourself back home -- but your car was still at work, your laptop
open etc.

Sure, if you're super careful you can make sure you handle the side effects of
what happened while the code was running, but if you forget one you have a
problem. Why not use the language features designed to take care of those
problems for you?

This sort of works in a pool-based memory allocator.

The failures happen three ways: one is you forget something and so you have a
leak. The second is that you haven't registered usage properly so have a
dangling pointer. Third is by going back in time you lose access to and the
value of prior and/or partial computation.

If you use this for a library, and between the setjmp and longjmp is entirely
in a single invocation you can sometimes get away with it. But in a thing like
a memory allocator where the user makes successive calls, unless you force the
user to do extra work you can't be sure what dependencies on the memory might
exist. If your library uses callbacks you can be in a world of hurt.

Trying to keep track of all those fiddly details is hard. C++ does it
automatically, at the risk of potentially being more careful (e.g.
deallocating two blocks individually rather than in one swoop -- oh, but that
language has an allocator mechanism precisely to avoid this problem). The
point is the programmer doesn't have to remember anything to make it work.

------
platz
> Composing Errors Codes ... Instead of sprinkling if statements, the error
> handling can be integrated into the type ... The check for errors is only
> done once.

That is only a superficial level of composition, if one can call it that at
all, that doesn't account for actual composition of errors of different types.
The example provided is just encapsulation and therefore orthogonal to the
issue of error handling approaches. i.e. in the example, the error handling
code is only centralized, not composed.

~~~
dannas
Can you make the distinction between "centralization" vs "composition" of
errors?

Do you mean the fact that there must be some if-statement within the API that
reacts to the different errors and sets a flag used by the Err() method?

Is you opinion that "composition of errors" always requires special syntactic
elements such as the match statement?

The code from the blog section:

    
    
      scanner := bufio.NewScanner(input)
      for scanner.Scan() {
          token := scanner.Text()
          // process token
      }
      if err := scanner.Err(); err != nil {
          // process the error
      }

------
nemothekid
The only downside of error codes via Sum types (Rust) seems to be, according
to the article, is performance. It then claims that Checked Exceptions are the
solution (at least according to Joe Duffy).

Maybe I'm naive to how exceptions are actually implemented, but it seems to me
that both a checked exception and Sum Type would incur the same overhead, a
single branch to make sure things haven't exploded.

~~~
barrkel
If you want to treat your error result as a first class value, and transport
it around, then your sum type can't use the same implementation as exceptions,
which can use data-driven stack unwinding to have zero cost in the success
case, the data being generated by the compiler and consumed by a stack
unwinder after it has been invoked by an exception raise.

------
identity0
The obvious solution (in C++) is not to use exceptions at all, but make your
own `error` and `expected<T>` class, and just add [[nodiscard]] to them. All
the benefits of Go-style errors, you’ll never forget to check the error, and
there is very little runtime overhead. If you pass the error as an out
parameter then there is zero runtime overhead on success.

~~~
dannas
Speaking of C++ exceptions: Andrei Alexandruesco has investigated the
performance impact of replacing exceptions with error codes. Dave Cheney made
a summary of Andreis points in [https://dave.cheney.net/2012/12/11/andrei-
alexandrescu-on-ex...](https://dave.cheney.net/2012/12/11/andrei-alexandrescu-
on-exceptions)

* The exceptional path is slow (00:10:23). Facebook was using exceptions to signal parsing errors, which turned out to be too slow when dealing with loosely formatted input. Facebook found that using exceptions in this way increased the cost of parsing a file by 50x (00:10:42). No real surprise here, this is also a common pattern in the Java world and clearly the wrong way to do it. Exceptions are for the exceptional. * Exceptions require immediate and exclusive attention (00:11:28). To me, this is a killer argument for errors over exceptions. With exceptions, you can be in your normal control flow, or the exceptional control flow, not both. You have to deal with the exception at the point it occurs, even if that exception is truly exceptional. You cannot easily stash the first exception and do some cleanup if that may itself throw an exception.

~~~
josefx
> You cannot easily stash the first exception and do some cleanup if that may
> itself throw an exception.

You can stash/rethrow exceptions since c++11 with an exception pointer if you
really need to.

[https://en.cppreference.com/w/cpp/error/exception_ptr](https://en.cppreference.com/w/cpp/error/exception_ptr)

------
ridiculous_fish
> Swift does not AFAICT provide mechanisms for enforcing checks of return
> types

Swift does this by default! You have to annotate (via @discardableResult)
those functions which should _not_ warn.

But of course try/catch is used in Swift more often.

~~~
dannas
Oh, that was sloppy of me. I should have read up more on Swift (I've never
used it myself).

While I have your attention: A big thank you for Fish shell!

And related to the current subject: How does fish handle errors? A quick skim
found some constants that are returned upon failure, such as this case for
disown: [https://github.com/fish-shell/fish-
shell/blob/master/src/bui...](https://github.com/fish-shell/fish-
shell/blob/master/src/builtin_disown.cpp#L23)

What trade-offs did you face when designing error handling for your shell?

~~~
ridiculous_fish
Thank you for the great article! You ask a good question.

Shells are rarely CPU bound, so some perf overhead is acceptable. But shells
may be used to recover badly broken systems. If fork or pipe fails, most
programs are OK to abort, but a shell may be the user's last hope, so has to
keep going.

For example, if pipe() fails, it's probably due to fd exhaustion. If your
system is in that state, the best thing to do is _immediately_ unwind whatever
is executing, and put the user back at the prompt. fish uses ad-hoc error
codes (reflecting its C legacy) instead of exceptions, though it uses RAII for
cleanup. Your question made me realize that fish needs a better abstraction
here; at least use `nodiscard`.

The story is different for script errors [1]. If the user forgets to (say)
close a quote in a config file, fish will print the line, a caret, and a
backtrace to the executing script. A lot of effort has gone into providing
good error messages with many special cases detected. The parser also knows
how to recover and keep going; I think Fabien would approve.

1: [https://github.com/fish-shell/fish-
shell/blob/225470493b3cd1...](https://github.com/fish-shell/fish-
shell/blob/225470493b3cd1142ad1206e0def1ac0b86d1e62/src/parse_constants.h#L167)

~~~
dannas
Yes, providing good error message is a hard problem. Writing a parser that
just bails out when it encounters and error is easy. Writing one that provides
error messages that are useful to the user is a _lot_ more work. That's a
dimension I didn't touch on in the article.

I would be interesting to do a follow-up to this post where I compare the
error handling for some common libraries/programs and ask the authors on what
trade-offs the faced when designing error handling. It's a subject, I
beliveve, that is often overlooked.

------
ensiferum
There are 3 separate things that each require their different approach.

\- errors i.e bugs made by programmer \- logical "error" conditions that the
program is expected to handle for example network connection failed or user
input failed \- unexpected error conditions that typically boild down to
resource allocation errors, socket could not be allocated or memory allocation
failed etc.

In my experience all of these are best handled with a different tool.

For bugs use an assert that dumps the core and leaves a clear stack trace. For
conditions that the program needs to handle use error codes. And finally for
the truly unexpected case use exceptions .

~~~
tsimionescu
Why dump core when you can log the bug and continue? Sure, in development we
want things to fail fast and loud, but when deployed with a customer, I don't
want my whole program to crash because there is one obscure code path that has
a problem.

And even for conditions that the program is expected to handle, 99.9% of the
time all it can do is notify the user and ask for guidance, which means that
the error must be bubbled up from a networking or storage layer all the way to
the presentation layer - a perfect task for exceptions or something like an
error monad.

The only problem with exceptions or error monads is that they get tricky in
the presence of resources that need to be released, and even that is well
handled with patterns like RAII.

~~~
wvenable
> Why dump core when you can log the bug and continue?

I see from your replies what you're trying to say. If an error occurs, most
likely you want the entire operation to abort -- that doesn't necessarily mean
the whole program depending on the program.

For example, if I have a GUI app and the "save" operation fails and I
typically roll that back right to the event loop of the application and the
user gets an error and they can retry the save.

For other types of applications, killing the whole process is ending the
operation.

~~~
tsimionescu
Yes, exactly!

And on the other end of spectrum, there are even systems where it makes sense
to go further than killing a single process, and kill the whole container or
even VM where a buggy condition was encountered.

------
gumby
An important paper on the trade offs: [http://www.open-
std.org/jtc1/sc22/wg21/docs/papers/2018/p070...](http://www.open-
std.org/jtc1/sc22/wg21/docs/papers/2018/p0709r0.pdf)

------
bestouff
Rust shines when doing error handling. No way to ignore errors, but properly
handling them often adds just a single question mark to your code. Everything
stays readable and lightweight.

Of course the error handling story is still perfectible but so far it's
already one of the best I know.

~~~
Thaxll
You can "ignore" error is Rust using _ like in Go.

~~~
Groxx
Not really. In Go you can `val, _ := func()` and use the value _even if there
is an error_. AFAIK there is no equivalent in Rust (for Option) outside of
unsafe shenaniganry. You can choose to panic / return err / etc, but you can't
choose to use the value regardless of the presence of an error.

~~~
MaulingMonkey
You can return tuples from Rust fns just like you would in Go, if that's your
thing - no unsafe necessary:

    
    
      fn foo() -> (usize, Result<(),()>) { (0, Ok(())) }
      let (a, err) = foo(); err?; // propigate error
      let (b, _) = foo(); // discard error
    

Or more typically, you might use one of the many fn s such as unwrap_or,
unwrap_or_else, unwrap_or_default, etc. - to provide your own appropriate
default value. I usually find that _useful_ default values are often caller
specific anyways (and doesn't require remembering which fns return which
default values on error):

    
    
      fn foo() -> Result<usize> { Ok(1) }
      fn bar() -> Option<usize> { None }
      let val = foo().unwrap_or(3); // val == 1
      let val = bar().unwrap_or(4); // val == 4
    

Alternatively you can use out parameters, which occasionally crops up in
Rust's own stdlib:

    
    
      let mut line = String::new();
      let _ = buf_read.read_line(&mut line);
      // ...use line, even if there was an error...
    

Also, the "error" type might contain values itself, although you're certainly
not ignoring the error if you use it:

    
    
      // https://doc.rust-lang.org/std/ffi/struct.OsString.html#method.into_string
      match os_string.into_string() {
          Ok(string) => println!("Valid UTF8 string: {}", string),
          Err(os_string) => println!("Invalid UTF8: {:?}", os_string),
      }

~~~
Groxx
Sure, it's always possible to design a system that avoids the type system of a
language. At worst case you can just resort to creating an un-typed lambda
calculus and re-implementing your logic there.

Community habits and language frictions matter _a lot_. In Go, doing the
equivalent of `Result` requires custom private types for every combination,
with private fields and accessor methods that protect against misuse. And you
_still_ only gain runtime safety. And they can _still_ make a zero-valued var
and use it without "initialization" (unless you use private types, which are
an even bigger pain for a variety of reasons). Any other approach makes it
trivial to bypass - public fields can just be accessed, multiple returns can
be ignored, etc. In Rust, the community and language make `Result` common, and
then you gain _compile-time_ safety in most cases, and warnings in all others
(AFAIK, as Result is annotated as "must use", so you get a warning or a `_ =`
as a visible marker that you're ignoring something).

\---

tl;dr multiple returns of course bypass this, but in practice you won't see
that in Rust unless it's _intended_ to allow ignoring the error. Underscores
on out-param funcs are a good point, but they're also fairly rare / serve as a
visual warning of shenaniganry.

------
hifly
No discussion is complete without mention of Erlang’s view on this

[https://erlang.org/download/armstrong_thesis_2003.pdf](https://erlang.org/download/armstrong_thesis_2003.pdf)

~~~
haihaibye
Erlang's error handling is mentioned in the article. Maybe read it before
posting?

