nox.im · All Posts · All in Go
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.