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