Better Go error handling

A peek at the xerrors package for Go

When it comes to errors in Go, you can fall into one of a few camps:

  1. you hate writing constant if/else blocks
  2. you think writing if/else blocks makes things clearer
  3. you don’t care because you’re too busy writing code

I fall somewhere between #2 and #3 which is why I was surprised that I was (seemingly) super excited when I saw the xerrors package. For those who haven’t heard, the xerrors package is slated for inclusion as part of the Go standard library in Go 1.13.

There is plenty to unpack, so I figured it could be good to give a bit of an overview. Hopefully once we’ve gone through a few scenarios it’ll all be clear.

Simple error handling

We’re going to work through a simple code example that just compares a trivial error with a predefined error (in reality, it’d be a set of errors) so that we can deal with specific error scenarios.

var ReallyBadError = errors.New("this is a really bad error")
func someErrorHappens() error {
return xerrors.Errorf(
"uh oh! something terrible happened: %w", ReallyBadError
)
}

In this scenario, we have a function that would hypothetically perform some action and then return specific errors based some kind of control flow. For example: Look up a user and return a UserNotFoundError.

In this code you’ll see a call to xerrors.Errorf. This call lets us add some context into our simple error message and then wraps the original error into the one we’re going to return. That’s what the %w flag is doing there.

Just to quickly explain that:

If the last argument is an error and the format string ends with “: %w”, the returned error implements Wrapper with an Unwrap method returning it.

This is particularly helpful for simple error handling and avoids having to manually store the chained error yourself. However, it is a “little bit of magic” so use wisely.

Now that that is all done, we can check the error conditions.

err := someErrorHappens()
if xerrors.Is(err, ReallyBadError) {
// deal with the really bad error
}

xerrors will automatically unwrap the error in this case and we can check the error chain to confirm that it is the specific error type we’re looking for. Pretty cool.

More complex error handling

Simple error checking is often enough, but, let’s move on to a more complex scenario. If you’re working with an API, there may be times when you want to return a code, a message or some other data. That’s pretty hard to do with just a plain old error. Often I see / hear about people parsing the error string or doing sub-string matching. That makes everyone a sad panda.

There is a better way!

Let jump into an example:

type ComplexError struct {
Message string
Code int
frame xerrors.Frame
}

This is our struct where we’re going to store an application specific Code and a helpful Message describing the situation. It could, of course, contain other things.

Next, we want to integrate this struct into our code but we want to do it in a way that is consistent with the standard error mechanisms in Go. This way, our code is easily tested and compatible with pretty much every other package.

It’s a pretty easy few steps to do that.

func (ce ComplexError) FormatError(p xerrors.Printer) error {
p.Printf("%d %s", ce.Code, ce.Message)
ce.frame.Format(p)
return nil
}
func (ce ComplexError) Format(f fmt.State, c rune) {
xerrors.FormatError(ce, f, c)
}
func (ce ComplexError) Error() string {
return fmt.Sprint(ce)
}

The first function, FormatError, is part of the Formatter interface of the xerrors package. This allows us to define how the error will be represented when we print it to the console or otherwise want to retrieve the simple / default value. The other functions implement Go’s standard error and formatting interfaces.

Here is the detailed breakdown of what is going on:

  • p.Printf will print a simple message to the Printer object. This will be what you see when you Println or use %s/%v in a formatted print statement.
  • The ce.frame.Format(p) statement prints the associated error frame. the Format function will automatically consider whether you’re asking to print “the details” or not (i.e.: %+v vs %v). If you don’t want the details, it won’t print the frame information.
  • The Format and Error functions provide backwards compatibility. In the case of Format, we’re just passing it all back into the xerrors mechanism.

The long story short here is that all of this is to tell Go how we want our custom error to be represented.

In our basic example, we will implement a quick function that just returns our ComplexError struct.

func someComplexErrorHappens() error {
complexErr := ComplexError{
Code: 1234,
Message: "there was way too much tuna",
frame: xerrors.Caller(1), // skip the first frame
}
return xerrors.Errorf(
"uh oh! something terribly complex happened: %w", complexErr
)
}

Our function still returns an error and not our custom struct and that’s a good thing because we can keep all of functions consistent (not a bunch of different structs everywhere). It also means that our code is compliant with all the rest of the code / libraries we’re going to be using.

OK, we know this is all good, but how do we get at the error and the code? Glad you asked.

cerr := someComplexErrorHappens()
var originalErr ComplexError
if xerrors.As(cerr, &originalErr) {
// deal with the complex error
// we can now directly interrogate originalErr.Code
// and originalErr.Message!
}

We can use the xerrors.As function to cast our error value to our custom struct. From there, we can interrogate the values directly. Pretty neat.

You could also store a raw error inside the struct if you’re wrapping an error from another library. This way you can continue the error chain.

Well .. that was actually a lot more content than what I originally though it would take. Hopefully I’ve helped a little! This is all coming in Go 1.13 (at least that’s the plan) but you’ll still need to use the xerrors package if you’re targeting Go versions that are lower. I think once you’ve tried it out you’ll switch!

About Yakka
Yakka is a digital product design and development agency. Whether it’s the web, Flutter, native mobile apps or a back-end project, we’re the ones who’ll get it done for you.

Reach out today!