Thinking About New Ways of Error Handling in Go 2

Peter Götz
4 min readSep 4, 2017

Recently, I was doing some refactoring on existing Go code and eventually ended up with this code:

Presented in this isolated way, it’s probably easy to spot that there is a bug in the error handling. Seeing this in a big junk of code, however, is a different story.

It’s obvious how this happened: a simple copy-and-paste mistake. Not a big problem in this little example. This is a CLI utility and if the server flag is not set, it would simply die with a panic saying something like “nil pointer dereference” and give me a stack trace which helps to solve the problem.

But in the production code of a critical service the effects of such a mistake can be pretty harmful.

One could argue that following best practices by writing a test could have helped. True. But while I’m a firm believer in writing tests, reality is, it’s not always possible or not always practical to write a test. Furthermore, Go is a statically typed language and it’s fair to put a certain amount of static correctness checking into the compiler.

I have been thinking about error handling more than once in the past. What I’d like to do in this post, is to propose a new approach to error handling. It’s neither exception based nor return code based. It reuses some of the concepts, but essentially tries to match what I will describe as “requirements for a sane error handling experience”.

Requirements for a Sane Error Handling Experience

The error handling mechanism should:

  • Force the developer to handle errors: Traditional error code mechanism as in C do not force the developer to handle the error. Developers can easily forget to handle an error. It’s hence error prone (no pun intended). Go goes a step further by requiring to use an assigned error variable. However, it’s still too easy to mess up, as the following example shows, by not handling the first e:
  • Have local scopes for errors to avoid handling mistakes: Both examples above already describe this. We simply want to have the error only available while handling it, not afterwards.
  • Have an expressive syntax: making clear where an error path starts, and making that part as concise as possible helps reading the code and will improve developer productivity.
  • Have a simple syntax to “fail fast”: Start-up code or test code do not need to handle errors in a way that gives the caller the chance to recover. They are simply interested in bailing out quickly. Instead of the typical if e != nil { panic(e) }, it would be nice if this doesn’t need three lines.

Proposal

By introducing three new keywords we can satisfy all the requirements listed above. They are:

  • fails_with Used in the function signature to denote the error type of this function and simply formalizes the existing convention of making the last return value the error.
  • fail Used to return from a function by failing. It takes the error value as argument.
  • on_error Used in the calling code to react to an error.

Let’s have a look at this proposal in action through a minimal example:

By specifying the error through fails_with in the signature, the compiler can enforce an on_error block-statement on the caller side. e has the type of the argument in fails_with and gets assigned the error value. That value is only available in the on_error block. So accidental reuse of the error variable is not possible. The syntax is short and expressive, letting us handle the error where it happens.

By allowing to omit the curly braces, we could implement the fail fast version simply by:

result := SomeFunction(arg) on_error e panic(e)

Not requiring the curly braces could make the syntax short and concise in many other scenarios too:

result := SomeFunction(arg) on_error e fail e

FAQ

Introducing new keywords makes the language more complex. Isn’t there a better solution?

Introducing new keywords might not be the typical Go strategy. The language has been marketed specifically to be simple because there are so few constructs or keywords. However, keywords themselves do not make a language complex. What makes a language complex, is having many concepts. The introduced keywords here really just formalize what is best practice and an existing concept in Go already. The benefit comes from being less error prone.

Isn’t this just adding some syntactic sugar?

It seems like on first sight. But letting the compiler help you avoid forgetting to handle an error is more than just syntactic sugar. Also, scoping the error variable is a huge improvement over the current situation.

Isn’t this an exception mechanism in disguise?

The idea behind exceptions is to automatically propagate failure conditions and handle multiple exceptions at common places to avoid “littering” the code with error handling. Exceptions are mostly concerned about what type of error happened. I believe, and that probably goes in line with what the Go designers had in mind, that it is more important to know where an error happened, and under which context. That allows precise error handling, and subsequently more useful error handling. Anyone who starts doing precise error handling with exceptions ends up with code convoluted with try ... catch.

Feedback

I’d love to get feedback on this proposal. It’s obviously a very early draft based on some thoughts of mine. There’s a lot that could be improved, like e.g. should we change the keywords? Could we avoid some of them? What other things am I completely missing? Is there a way to simplify the constructs I introduced? Any thoughts in the comments section are welcome!

Update Nov. 13, 2018

Here’s an example what the popular CopyFile function would look like that is used in the current Go 2 draft design documents for Error Handling:

As can be seen, the flow is still linear (i.e. no jumping back and forth between some pre-set handler functions or similar). We still use the defer construct to handle automatic closing of the source file. No automatic/implicit error propagation, but a concise way to propagate errors explicitly.

And as a nice bonus, “command” functions, such as Copy and Close are not hidden in if constructs, but are easily identifiable as such.

--

--

Peter Götz

SDE at AWS. I breathe code. I'm blogging here privately, opinions are my own. https://petergoetz.dev