Go errors with additional details.

Roman Budnikov
2 min readMay 3, 2021

In Go, an error type is just an interface with Error() string signature, so every type that implements this signature satisfies it. Go also provides the errors package which allows creating so-called sentinel errors. You can find a bunch of them across all the standard library API and examples can be io.EOF ,sql.NoRows, etc.

You can create those type of errors by yourself using fmt.Errorf or errors.New .

ErrNotFound = errors.New("not found")

They come in handy in a lot of cases but when your codebase starts to grow you need to define more context to those errors to be able to reuse them.

After the release of the Go version 1.13 you should check those errors using errors.Is function that calls Unwrap method for the error until it returns nil or the error is equal to the error you’ve provided.

if errors.Is(err, ErrNotFound) {
...
}

Another kind of error is error types and those are any types that implement error interface signature.

type NotFoundError struct {
Type, Key string
}

func (e *NotFoundError) Error() string {
return fmt.Sprintf("%s with key %s not found", e.Type, e.Key)
}

Those errors give you the ability to provide more context to the error and be able to not just check the error but also handle it gracefully. For example, if your API has an endpoint that finds a resource by its id you can provide a reusable and meaningful response for the server including which resource hasn’t been found and its id.

After the release of the Go version 1.13 you should check those errors using errors.As function to Unwrap type error.

var nfe NotFoundError
switch {
case errors.As(err, &nfe)
...
}

The downside of this that you need to identify a variable for every type of errors you’re expecting to receive and if you have a function that handles general errors it this will force you to initialize more and more variables which you will need to assert using errors.As function

As you’ve can see already both errors.As and errors.Is are using Unwrap method for the function which gives the ability to combine both of those error types by wrapping sentinel error into a type error using the factory function.

var ErrNotFound = errors.New("not found")type NotFoundError struct {
Kind, Key string
origin error
}
// factory function for NotFoundError.
func NewNotFoundError(kind, key string) error {
e := NotFoundError{
Kind: kind,
Key: key,
origin: ErrNotFound,
}
return e
}
func (e NotFoundError) Error() string {
return fmt.Sprintf("%s with key %s not found", e.Kind, e.Key)
}
func (e NotFoundError) Unwrap() error {
return e.origin
}

This adds the ability to check these errors using errors.Is and assert it to the underlying type to get more context from it and handle this error as required.

--

--