Up to Main Index Up to Journal for September, 2025
JOURNAL FOR TUESDAY 30TH SEPTEMBER, 2025
______________________________________________________________________________
SUBJECT: Curious error technique…
DATE: Tue 30 Sep 13:26:30 BST 2025
Last week I was reading some code from a developer I will call Mike. While
doing so I came across a way of creating errors in Go I hadn’t considered
before. When something like that happens I take note, mull over the idea, play
around with it a bit and see if it makes sense and I like it. If I do it goes
into my little programmer’s toolbox of useful things :)
What was the code I came across? It was along the lines of:
var ErrInvalidLength = errors.New("invalid length")
⋮
⋮
return fmt.Errorf("%w: %d", ErrInvalidLength, L)
To my eyes seeing "%w: %d", with the %w on the left, seemed a little strange.
To find out why lets take a look at some error handling in Go.
Usually, for simple error handling, I might do something like:
return errors.New("invalid length")
If I wanted to include some additional context then maybe:
return fmt.Errorf("invalid length: %d", L)
In both cases we are just signalling a problem with a generic error. What if
we want to be able to check for a specific error? In that case we would create
a sentinel error:
var ErrInvalidLength = errors.New("invalid length")
⋮
⋮
if errors.Is(err, ErrInvalidLength) {
// handle length error
}
Errors are a lot more useful when there is some additional context. Typically
context is added by creating a new error type containing the details and that
satisfies the error interface:
type InvalidLengthError struct {
Length int
}
func (e *InvalidLengthError) Error() string {
return fmt.Sprintf("invalid length: %d", e.Length)
}
Let us assume we want a specific error we can test for. Let us also assume we
want additional context. However, the additional context is for logging for
humans to read - the code is not interested in the details. Is there a simpler
way than this:
package main
import (
"errors"
"fmt"
)
type InvalidLengthError struct {
Length int
}
func (e *InvalidLengthError) Error() string {
return fmt.Sprintf("invalid length: %d", e.Length)
}
func measure() error {
return &InvalidLengthError{42}
}
func main() {
err := measure()
if err != nil {
var ile *InvalidLengthError
if errors.As(err, &ile) {
fmt.Printf("size error: %s\n", ile)
} else {
fmt.Printf("unexpected error: %s\n", ile)
}
}
}
This is where Mike’s technique comes into play very nicely. How about:
package main
import (
"errors"
"fmt"
)
var ErrInvalidLength = errors.New("invalid length")
func measure() error {
return fmt.Errorf("%w: %d", ErrInvalidLength, 42)
}
func main() {
err := measure()
if err != nil {
if errors.Is(err, ErrInvalidLength) {
fmt.Printf("size error: %s\n", err)
} else {
fmt.Printf("unexpected error: %s\n", err)
}
}
}
Like all errors, such errors can be wrapped:
var (
ErrInvalidLength = errors.New("invalid length")
ErrInvalidSize = errors.New("invalid size")
)
⋮
⋮
err := fmt.Errorf("%w: %d", ErrInvalidLength, 42)
⋮
⋮
return fmt.Errorf("%w: %w" ErrInvalidSize, err)
⋮
⋮
if err != nil {
fmt.Println(err) // displays "invalid size: invalid length: 42"
}
We now have a very simple way of composing informative error messages.
Instead of:
return fmt.Errorf("invalid length: %s", L)
We can use:
var ErrInvalidLength = errors.New("invalid length")
⋮
⋮
return fmt.Errorf("%w: %d" ErrInvalidLength, L)
With the added benefit of being able to test for the specific error with:
if errors.Is(err, ErrInvalidLength) {
// handle length error…
}
It would be nice to be able to unwrap these errors and to be able to find the
text for a specific wrapped error. I came up with this little helper:
// FindError looks for the given target in err's tree. If target is found
// returns the error message for target including messages target may
// wrap. If target is not found returns an empty string.
func FindError(err, target error) string {
if err == target {
return err.Error()
}
var errs []error
switch e := err.(type) {
case interface{ Unwrap() error }:
errs = append(errs, e.Unwrap())
case interface{ Unwrap() []error }:
errs = append(errs, e.Unwrap()...)
default:
return ""
}
for _, e := range errs {
if e == target {
return err.Error()
}
if s := FindError(e, target); s != "" {
return s
}
}
return ""
}
Here is a complete, stand-alone program using these ideas:
package main
import (
"errors"
"fmt"
)
var (
ErrInvalidLength = errors.New("invalid length")
ErrInvalidSize = errors.New("invalid size")
ErrInvalidItem = errors.New("invalid item")
ErrNotFound = errors.New("not found")
)
func main() {
// Compose an error from wrapped errors...
err := fmt.Errorf("%w: %d", ErrInvalidLength, 42)
err = fmt.Errorf("%w: %w", ErrInvalidSize, err)
err = fmt.Errorf("%w: %s, %w", ErrInvalidItem, "belt", err)
// Try to find specific error messages...
fmt.Println(FindError(err, ErrInvalidLength))
fmt.Println(FindError(err, ErrInvalidSize))
fmt.Println(FindError(err, ErrInvalidItem))
fmt.Println(FindError(err, ErrNotFound))
// Check for a specific error
if errors.Is(err, ErrInvalidSize) {
fmt.Println("The size is invalid.")
}
}
// FindError looks for the given target in err's tree. If target is found
// returns the error message for target including messages target may
// wrap. If target is not found returns an empty string.
func FindError(err, target error) string {
if err == target {
return err.Error()
}
var errs []error
switch e := err.(type) {
case interface{ Unwrap() error }:
errs = append(errs, e.Unwrap())
case interface{ Unwrap() []error }:
errs = append(errs, e.Unwrap()...)
default:
return ""
}
for _, e := range errs {
if e == target {
return err.Error()
}
if s := FindError(e, target); s != "" {
return s
}
}
return ""
}
Running the above code would result in the following output:
invalid length: 42
invalid size: invalid length: 42
invalid item: belt, invalid size: invalid length: 42
// empty string here
The size is invalid.
I think this is quite neat, into the toolbox it goes. Thanks Mike!
--
Diddymus
Up to Main Index Up to Journal for September, 2025