-- Leo's gemini proxy

-- Connecting to nox.im:1965...

-- Connected

-- Sending request

-- Meta line: 20 text/gemini; charset=utf-8

Why Wrapping Go Errors and How to Unwrap


Since version 1.13, Go supports error wrapping[1]. 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.


1: error wrapping


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'[1], may encounter this particular example here:


1: Go ORM, entity framework 'ent'


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[1], 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:


1: 23505 unique_violation


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.


-- Response ended

-- Page fetched on Sat Apr 27 19:37:29 2024