Errors

How will we know that the program is behaving how we expect it to?

4-minute read
Table of Contents

As opposed to other languages like Python and JavaScript that use a try-catch approach to error handling, Go encourages us to return a separate value that will indicate that the code did not perform in an ideal manner - an error.

This way of handling errors ensures that dealing with errors are part of the normal execution flow of the code, instead of having a structure that can be completely left out, as is the case of a try-catch.

Convention

It is customary to return the error as the last value from a function, with the return type being error:

package main

import (
	"errors"
	"fmt"
)

func Div(a, b float64) (float64, error) {
	if b == 0.0 {
		return 0.0, errors.New("cannot divide by zero")
	}
	return a / b, nil
}

func main() {
	numbers := []int{3, 2, 0, -4, 5, -2}
	for _, f := range numbers {
		result, err := Div(4.0, float64(f))
		if err != nil {
			fmt.Printf("error: %v\n", err)
		} else {
			fmt.Printf("4/%d = %f\n", f, result)
		}
	}
}
errors-new.go
Copy

The function errors.New() is used to define a new error with a desired error message. We can use the fmt.Errorf function to define errors as variables in our code:

package main

import (
	"fmt"
)

var ErrNoZeroDivision = fmt.Errorf("cannot divide by zero")

func Div(a, b float64) (float64, error) {
	if b == 0.0 {
		return 0.0, ErrNoZeroDivision
	}
	return a / b, nil
}

func main() {
	numbers := []int{3, 2, 0, -4, 5, -2}
	for _, f := range numbers {
		result, err := Div(4.0, float64(f))
		if err != nil {
			fmt.Printf("error: %v\n", err)
		} else {
			fmt.Printf("4/%d = %f\n", f, result)
		}
	}
}
errors-fmt.go
Copy

Inline checking

We can check errors inline by separating the lines using a semi-colon:

package main

import (
	"fmt"
)

var ErrNoZeroDivision = fmt.Errorf("cannot divide by zero")

func Div(a, b float64) (float64, error) {
	if b == 0.0 {
		return 0.0, ErrNoZeroDivision
	}
	return a / b, nil
}

func main() {
	numbers := []int{3, 2, 0, -4, 5, -2}
	for _, f := range numbers {
		if result, err := Div(4.0, float64(f)); err != nil {
			fmt.Printf("error: %v\n", err)
		} else {
			fmt.Printf("4/%d = %f\n", f, result)
		}
	}
}
errors-inline.go
Copy

Our code can look more compact using this technique.

Custom errors

Any custom type can be used as an error as long as it implements the Error() method.

package main

import (
	"errors"
	"fmt"
)

type fancyError struct {
	arg     int
	message string
}

func (f *fancyError) Error() string {
	return fmt.Sprintf("%d - %s", f.arg, f.message)
}

func f(arg int) (int, error) {
	if arg == 42 {
		return -1, &fancyError{arg, "can't work with it"}
	}
	return arg + 3, nil
}

func main() {
	_, err := f(42)
	var ae *fancyError
	if errors.As(err, &ae) {
		fmt.Println(ae.arg)
		fmt.Println(ae.message)
	} else {
		fmt.Println("err doesn't match argError")
	}
}
custom-errors.go
Copy

errors.Is checks to see if the error matches a certain value specifically. errors.As checks to see if the given error or any errors in the chain of errors matches a specific error type.

Wrapping and Unwrapping errors

Define a new error and wrap it with another error:

package main

import (
	"errors"
	"fmt"
)

var ErrAppRunningSlow = errors.New("your application is slowing down")

// function to wrap ErrAppRunningSlow
func checkVisitors() error {
	return fmt.Errorf("too many visitors: %w", ErrAppRunningSlow)
}

// function to wrap the wrapped version of ErrAppRunningSlow
func checkVisitorsInGuyana() error {
	err := checkVisitors()
	return fmt.Errorf("Guyanese are visiting daily: %w", err)
}

func main() {
	err := checkVisitorsInGuyana()
	errInside := errors.Unwrap(err)
	errInsideInside := errors.Unwrap(errInside)
	someErr := errors.Unwrap(errInside)
	otherErr := errors.Unwrap(errInside)

	// print error wrapping the error wrapping ErrAppRunningSlow
	fmt.Println(err)
	// print error wrapping ErrAppRunningSlow
	fmt.Println(errInside)
	// print ErrAppRunningSlow (the innermost error)
	fmt.Println(errInsideInside)

	// print the innermost error
	fmt.Println(someErr)
	// print the innermost error
	fmt.Println(otherErr)
}
error-unwrap.go
Copy

Every call of the errors.Unwrap function, the inner error is returned. If the function is called with on the innermost error, it will keep on returning that error.

Support us via BuyMeACoffee