Stephan Schmidt

Musings about error handling mechanisms in programming languages

It seems no language got error handling right


When we write code, errors happen inside functions at the time we call other functions:

fn f() {
// Error can happen when b()
// returns an error
 a = b()
 ...
}

The problem that arises is that

  • sometimes we don’t want to deal with the error and just return from our function
  • sometimes we want to mitigate the error
  • sometimes we want to handle the error much later—for example, with other errors. Preferably, with normal control flow continuing.

Every programming language has found a different solution to these three challenges.

Java was one of the first mass languages to rise to a higher error management state with Exceptions 1. b can throw an exception on an error. The calling function can then do nothing, in which case the calling function f returns to its caller with the exception. Or it can deal with the exception later by wrapping the call in try/catch. The downside of the Java method is we can’t have normal control flow after the error occurred. Either we handle it or let it bubble up.

One of the downsides of the Java exception mechanism is the declaration of checked exceptions. If our function f() declares its exceptions, and the function b() throws different exceptions, we need to handle exceptions either way, because it can’t bubble up.

Rust found a solution to this with a mechanism to auto convert one error—that of b()—to another error - that of f(). This way again we can let the error bubble up without dealing with it. Rust uses ? for that

fn f() {
 // Let function f() return
 // error autoconvert and bubble up
 a = b()?
 ...
}

Some programming languages deal with the three challenges by returning an error code next to the value. One of these is Go.

a, err := b()

Now we can deal with the error

if err != nil { .... }

or return from our function. We can have normal program flow after the error—in the error case—unless we want to act on a:

a = a + 1

doesn’t work, if there was an error and a is nil.

We now could check for the existence of a every time

if a != nil { .... }

but this becomes cumbersome and unreadable fast.

Some programming languages deal with the problem of control-flow-after-error using Monads.

// a is of type Result<A,E>
a = b()

With the Result Monad in place, I can deal with the error or return from the method. As mentioned above, for returning Rust has some special syntax

a = b()?

With the question mark, the function will return at that line when b() returns an error and the error bubbles up with auto converting.

We can also have normal control flow in the case of error but still use a. Magic!

a = b()
c = a.map(|v| v + 1)

...
// Deal with error later

In the case of an error, c will be also be an error, otherwise c will contain the value of a increased by 1. This way we can have the same control flow after an error wether the error occured or not.

This makes reasoning about the code much easier.

Zig has a short notion of Result<A,E> by annotating a type with !.

// Returns i32
fn f() i32 {
...
}

// Returns i32 or an error
fn f() !i32 {
...
}

Zig also solves the Java problem of the tedious declaration of exception by flow analysis. It checks your function f() and find out all the errors it can return. Then, if you check for a specific error in the calling code, it makes sure it is exhaustive.

Rust with ? has a special syntax to return on the spot. Java has special syntax with try/catch to not return on the spot and return to the caller of the function if we don’t write additional code.

The question is: What do we do more often? Return on error or keep going? What we do have more often, should have the less verbose syntax. With the ? case in Rust, should we need a ? for returning on the spot, or ? to not return?

a = b()?

The ? can either mean, “return on error”. Or the behavior could be, always return on the spot if b() returns an error and the ? prevents this.

It depends on what happens more often.

Golang might give us another clue. It has special syntax for cleanup when a function returns

f := File.open("my.txt")
// Make sure we close the file
// on exiting the function
defer f.close()

a, err = b()

if err != nil {
  // f.close() is called here
  return
}

Java has something less elegant with finally. It looks like people think errors should bubble up, and we need some simple clean up in that case.

And from my experience, I also suspect we would want to let most errors bubble up with auto conversion, so the ? should perhaps signal that we don’t want the function to return, the opposite of what Rust is doing.

It seems Java was right with exceptions. No syntax means bubble-up behavior. It missed auto conversion and Exception<V,E> from Rust though - and a local, simple deferlike Go instead of the non-local, verbose finally of Java. And Java didn’t explain how to properly use exceptions, so everyone used them the wrong way.

What about a hypothetical language like this:

fn f() {
  // b() returns Result<V,E> or !V in Zig,
  // f() returns if b is an error
  // a is of type V
  a = b()

  // do not return on error but
  // a is of type Result<V,E> or !V
  a = b()!

  // compiles to a = a.map(|v| v + 1)
  a = a + 1

  // compiles to c = a.map(|v| v.c())
  // c is of type Result<C,E>
  c = a.c()
  ...
}

This has a much higher readability.

What should we do though when calling another method?

// Does not work if d expects
// C as a parameter type
// and not Result<C,E>
d(c)

Some languages have a special language syntax to deal with the problem E.g. Haskell has do and Scala has for. But then you have special code arround errors, and a special context. Which makes things harder to read again, contrary to the intentions.

So it’s best to throw a compiler error. And remember, the default way is to bubble up and a being of type V.

We can ease the pain with control flow analysis. Some programming languages like TypeScript do someting like this

a = b()
a = a + 1 // A is still Result<V,E>
if a instanceof Error {
 return
}
// A is now of type V
// because we checked for an error
d(a)

It looks like every programming language holds a piece to the optimal error handling puzzle. None has succeeded from what I can see.

That’s All Folks.


  1. I’ve used ON ERROR GOTO in BASIC which is some kind of exception handling - with one of the problems right there in the GOTO - and LISP did everything much earlier ↩︎

Other Articles

Tests Are Bad For Developers

Let New Hires Write A Todo App

The Mysterious Case of Lost Developer Productivity

Selfhealing Code for Startup CTOs and Solo Founders

The AI Manager - The End of Programming