Typed Go errors

By Richard Crowley

Let’s start with an unpopular opinion:  I like Go’s options for handling errors.  Situations that are truly irrecoverable and, if we’re honest with ourselves, usually indicate there’s a bug lurking somewhere nearby are quickly and safely handled by a panic that crashes the program.  The remaining errors — a larger and more interesting population by far — are propagated through our programs in error values.

It’s cheap to say that most of the characters in Go source code are “if err != nil {” as if that’s the punch line to a classic joke.  I prefer to think of these parts of Go programs a little more charitably.  An error result parameter is a function’s opportunity to inform the caller what happened.  nil tells us that what we hoped would happen did, in fact, happen.  Non-nil values break the bad news to us.  In some situations, returning early and propagating the error is the right thing to do.  In other situations, whether the error was a failure or a timeout, DNS or HTTP, recoverable or presumed fatal, etc. changes how we handle it.

I wish more Go programs used the (potentially) rich type information available behind the built-in error interface.  net.Error and the types that implement it are good examples from the standard library that show the potential power of the type system when handling, rather than merely propagating, errors encountered by running Go programs.

It is really easy to add rich type information to errors in your programs.  Whenever you feel like using errors.New or fmt.Errorf, take a moment to add something like this to your program, instead:


type MyError string

func (err MyError) Error() string {
	return fmt.Sprintf("MyError: %v", string(err))
}

Once you have this, using it is as simple as return MyError("bad thing").  Go ahead, memorize this.  Type it early, type it often.

You can take it a step further, too, and wrap other errors with both a string prefix for your logs and a type for your program:


type MyWrappedError struct{ error }

func (err MyWrappedError) Err() error {
	return err.error
}

func (err MyWrappedError) Error() string {
	return fmt.Sprintf("MyWrappedError: %v", error(err.error))
}

Here, too, usage is simple, though braces are required instead of parentheses:  return MyWrappedError{err}.  (Note well that wrapping a nil error in this manner will make it non-nil when it’s used.)  Preserving type information preserves our ability to make good error handling decisions later in our programs.

With some help from the type system, error handling will never have to be limited to equality comparison or resort to substring extraction.


switch err.(type) {
case MyError:
	fmt.Print("handling MyError specially\n\n")
default:
	fmt.Print("nothing special to see here\n\n")
}

if wrapped, ok := err.(MyWrappedError); ok {
	fmt.Printf("type:  %T\nvalue: \"%v\"\n\n", wrapped.Err(), wrapped.Err())
}

(These two examples are from https://play.golang.org/p/j6SxDUPHrai.)

if err != nil {” doesn’t have to be the end of the error handling story.  The type system can open up expressive and safe possibilities and all it costs are a few lines of code like these:


type MyError string

func (err MyError) Error() string {
	return fmt.Sprintf("MyError: %v", string(err))
}

Type them early, type them often.