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