nox.im · All Posts · All in Go

Why Wrapping Go Errors and How to Unwrap

Since version 1.13, Go supports error wrapping. It was added to overcome the challenges of string parsing which lead to coupling of packages and allows for location information in the call stack. An error e can wrap another error w by providing an Unwrap method that returns w. Both e and w are available to programs, allowing the former to provide additional context to w or to reinterpret it while still allowing programs to make decisions based on w. This is best explained by some examples.

Remember that Go errors are handled by checking return values from a functions and propagate the error to higher layers with returns (optionally adding details to the error). Example

// https://github.com/ent/ent/blob/v0.8.0/dialect/sql/sqlgraph/graph.go#L864-L866
if err := c.insert(ctx, tx, insert); err != nil {
    return fmt.Errorf("insert node to table %q: %w", c.Table, err)
}

A CRUD operation, e.g. from the Go ORM, entity framework ‘ent’, may encounter this particular example here:

dre, err := client.User.
    Create().
    SetName("dre").
    AddGroups(g1, g2).
    Save(ctx)

If the client is based on PostgreSQL and the user with the name dre already exists, we will most likely get a unique constraint violation returned. If we print this error we see

ent: constraint failed: insert node to table "users": pq: duplicate key value violates unique constraint "users_name_key"

This indicates the error is 3 times wrapped

ent:
    constraint failed: insert node to table "users":
        pq: duplicate key value violates unique constraint "users_name_key"

The first two just add location information, lower two enrich the error with information. What we get on the surface however, is an error of type ent.ConstraintError, beneath is the fmt.wrapError and on the bottom layer is the actual problem, pq.Error.

If we now want to check if this is a 23505 unique_violation, we can use errors.As(err, target) to find the first error in the chain that matches the target interface, and set target to that error value. If the error is present in the chain, As returns true. Concretely, we can check for this error as follows:

var e *pq.Error
if errors.As(err, &e) {
    switch e.Code {
    case "23505":
        return true
    // ...
    }
}

At its core, this means that instead of type asserting errors or worse, string matching errors

if e, ok := err.(*pq.Error); ok {...
if strings.Contains(err.Error(), "unique_violation") { ...

we can write write

var e *pq.Error
if errors.As(err, &e)

This is already very powerful. Note, don’t make the mistake of allocating a new error e := &pq.Error{}. Even if the two may have the same values, they will be two different points in memory and the function will not return true.

Further, let’s assume a client server setup. If both are written in Go, the client might receive a string from the server that it likes to match to the statically typed error from a package. Even if we convert the string to the error type, the error won’t match with ==. However, we can use errors.Is(err, target). I.e. instead of

if err == io.ErrUnexpectedEOF

we can write

if errors.Is(err, io.ErrUnexpectedEOF)

This is again very powerful.

In order to make your packages allow the caller to access errors properly through errors.Unwrap, errors.Is or errors.As, change fmt.Errorf("... %v", err) to fmt.Errorf("...%w", err). It constructs the string in the same way as it did before, but it also wraps the error.

For types that implement error, add the Unwrap() method.

type SomeClientPackageError struct {
	someInfo string
	err  error
}

func (e *SomeClientPackageError) Error() string { return e.someInfo + ": " + e.err.Error() }

func (e *SomeClientPackageError) Unwrap() error { return e.err }

The type will then work with the Is and As functions.


Published on Friday, Jul 2, 2021. Last modified on Sunday, Jan 9, 2022.
Go back

If you’d like to support me, follow me on Twitter or buy me a coffee. Use Bitcoin
BTC address: bc1q6zjzekdjhp44aws36hdavzc5hhf9p9xnx9j7cv