DEV Community

Chris Wendt
Chris Wendt

Posted on

Ergonomic error handling in Go using generics

Now that Go has generics, I discovered that you can implement ergonomic exception-like error handling as a library. Here's my take on the idea:

https://github.com/chrismwendt/go-exceptions

Typical error-handling in Go looks like this:

func f() error {
  err := g1()
  if err != nil {
    return err
  }

  v1, err := g2()
  if err != nil {
    return err
  }

  // ...
}
Enter fullscreen mode Exit fullscreen mode

There are a few downsides to handling errors like this:

  • if err != nil { return err } takes 3 extra lines
  • You need to explicitly store the errors in variables and conditionally return them every time you call a function that returns an error
  • Error handling ends up cluttering the code and making it harder to focus on the domain logic
  • You can't do f(g()) if g() returns a value and an error and f() expects only a value, which forces you to split up the code, add temporary variables, add an if, and manually handle the error

How can we make this more ergonomic? Perhaps ideally, Go would add support for Rust's ? operator, but we can get pretty close using panic() and recover() to have errors bubble up without needing to explicitly store them in variables.

Here's what usage of go-exceptions looks like:

import ex "github.com/chrismwendt/go-exceptions"

func f() (err error) {
  // Catch() calls recover() and assigns the error to &err
  defer ex.Catch(&err)

  // Throw() calls panic() if the error argument is not nil
  ex.Throw(g1())

  // Throw() also accepts a label
  ex.Throw(g2(), "g2")

  // Throw1() returns 1 value, Throw2() returns 2 values, etc.
  v1 := ex.Throw1(g3())

  // Passing arguments is easier
  v2 := ex.Throw1(g4(ex.Throw(g5())))

  // ...
}
Enter fullscreen mode Exit fullscreen mode

This is more ergonomic in a few ways:

  • It eliminates a bunch of temporary err variables
  • You can more easily pass the result of one function into another
  • The code is more compact
  • Once you learn about the library, IMO the code is more readable

Internally:

  • Throw(err) is basically: if err != nil { panic(err) }.
  • Catch(*error) recovers from any panic and stores the recovered value in the given error

You could go a bit further and adopt a convention of panicking rather than returning errors, but that begins to depart from typical Go calling conventions and could cause friction when calling or being called by other code.

Top comments (0)