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:
- you hate writing constant if/else blocks
- you think writing if/else blocks makes things clearer
- 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 thePrinter
object. This will be what you see when youPrintln
or use%s/%v
in a formatted print statement.- The
ce.frame.Format(p)
statement prints the associated error frame. theFormat
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
andError
functions provide backwards compatibility. In the case ofFormat
, we’re just passing it all back into thexerrors
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!