-- Leo's gemini proxy
-- Connecting to nox.im:1965...
-- Connected
-- Sending request
-- Meta line: 20 text/gemini; charset=utf-8
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.
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:
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:
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