DEV Community

Nicola Apicella
Nicola Apicella

Posted on

Golang error updates

Since go 1.20, golang has improved the ergonomics of error handling. Noteworthy improvements include:

  • errors.Is and errors.As have been updated to work on tree of errors
  • fmt.Errorf accepts multiple %w format verbs, allowing to join multiple errors
  • The Unwrap []error function allows traversing through tree of errors

Follow some tests demonstrating the practical application of these enhancements (also available in the golang playground):

package main

import (
    "errors"
    "fmt"
    "testing"
)

func TestErrorfForWrapping(t *testing.T) {
    err1 := errors.New("a")
    err2 := errors.New("b")
    // the parentheses in the string are arbitrary
    wrap := fmt.Errorf("%w (%w)", err1, err2)

    if errors.Is(wrap, err1) == false {
        t.Fatal()
    }
    if errors.Is(wrap, err2) == false {
        t.Fatal()
    }
}

func TestStatusCodeInErrors(t *testing.T) {
    var (
        //
        // client errors
        //
        clientError = errors.New("client error")
        //
        // client error causes
        //
        unauthorized     = errors.New("unauthorized")
        resourceNotFound = errors.New("resource not found")

        //
        // server errors
        //
        serverError = errors.New("server error")
        //
        // server error causes
        //
        badGatewayError     = errors.New("badGateway")
        internalServerError = errors.New("internal server error")
    )

    getStatusCode := func(err error) (int, string) {
        if errors.Is(err, clientError) {

            if errors.Is(err, unauthorized) {
                return 400, "unauthorized"
            }
            if errors.Is(err, resourceNotFound) {
                return 400, "resource not found"
            }
            return 400, "no error type"

        } else if errors.Is(err, serverError) {

            if errors.Is(err, badGatewayError) {
                return 500, "bad gateway"
            }
            if errors.Is(err, internalServerError) {
                return 500, "internal server error"
            }
        }
        return 500, "no error type"
    }

    err := fmt.Errorf("%w (%w)", clientError, unauthorized)

    if code, mex := getStatusCode(err); code != 400 || mex != "unauthorized" {
        t.Fatal()
    }

    err = fmt.Errorf("%w (%w)", clientError, resourceNotFound)
    if code, mex := getStatusCode(err); code != 400 || mex != "resource not found" {
        t.Fatal()
    }

    err = fmt.Errorf("%w (%w)", serverError, internalServerError)
    if code, mex := getStatusCode(err); code != 500 || mex != "internal server error" {
        t.Fatal()
    }
}

type customError struct {
    wrapped []error
}

func (t *customError) Unwrap() []error {
    return t.wrapped
}
func (t *customError) Error() string {
    return "custom error"
}

func TestCustomErrorWrapping(t *testing.T) {
    err1 := errors.New("a")
    err2 := &customError{wrapped: []error{err1}}
    // it's much more convenient to do this
    // wrap := fmt.Errorf("%w (%w)", err1, err2) and you do not need to implement Unwrap

    if errors.Is(err2, err1) == false {
        t.Fatal()
    }
    if errors.Is(err2, err2) == false {
        t.Fatal()
    }
    if errors.Is(err2.Unwrap()[0], err1) == false {
        t.Fatal()
    }
}

Enter fullscreen mode Exit fullscreen mode

Top comments (0)