🎓 A1Learner
🔍
0 of 34 lessons completed

Go Sentinel Errors

A sentinel error is a package-level error value that code returns for one named failure. Callers test for that exact value instead of reading the message text.

A package-level sentinel error

ErrTooSmall is declared once at package level. check returns that exact value for a small number.

package main

import (
	"errors"
	"fmt"
)

var ErrTooSmall = errors.New("value too small")

func check(n int) error {
	if n < 10 {
		return ErrTooSmall
	}
	return nil
}

func main() {
	err := check(4)
	fmt.Println(err)
	fmt.Println(errors.Is(err, ErrTooSmall))
}

errors.As extracts a custom error type

LimitError carries the limit and the given value as fields. errors.As copies the error into a variable so the fields can be read.

package main

import (
	"errors"
	"fmt"
)

type LimitError struct {
	Limit int
	Got   int
}

func (e LimitError) Error() string {
	return fmt.Sprintf("value %d is over the limit %d", e.Got, e.Limit)
}

func check(n int) error {
	if n > 100 {
		return LimitError{Limit: 100, Got: n}
	}
	return nil
}

func main() {
	err := check(140)
	var limitErr LimitError
	if errors.As(err, &limitErr) {
		fmt.Println("Limit:", limitErr.Limit)
		fmt.Println("Got:", limitErr.Got)
	}
}

Is for the sentinel and As for the type

One check function can return either failure shape. errors.Is tests the sentinel case and errors.As tests the typed case.

package main

import (
	"errors"
	"fmt"
)

var ErrTooSmall = errors.New("value too small")

type LimitError struct {
	Limit int
	Got   int
}

func (e LimitError) Error() string {
	return fmt.Sprintf("value %d is over the limit %d", e.Got, e.Limit)
}

func check(n int) error {
	if n < 10 {
		return ErrTooSmall
	}
	if n > 100 {
		return LimitError{Limit: 100, Got: n}
	}
	return nil
}

func main() {
	fmt.Println(errors.Is(check(4), ErrTooSmall))
	var limitErr LimitError
	fmt.Println(errors.As(check(140), &limitErr))
	fmt.Println(limitErr.Got)
}

Example

package main

import (
	"errors"
	"fmt"
)

var ErrTooSmall = errors.New("value too small")

type LimitError struct {
	Limit int
	Got   int
}

func (e LimitError) Error() string {
	return fmt.Sprintf("value %d is over the limit %d", e.Got, e.Limit)
}

func gate(n int) error {
	if n < 10 {
		return ErrTooSmall
	}
	if n > 100 {
		return LimitError{Limit: 100, Got: n}
	}
	return nil
}

func main() {
	err := gate(4)
	if errors.Is(err, ErrTooSmall) {
		fmt.Println("too small")
	}
	err = gate(150)
	var limitErr LimitError
	if errors.As(err, &limitErr) {
		fmt.Println("over limit by", limitErr.Got-limitErr.Limit)
	}
}

Key points

  • Use a package-level var with errors.New to declare a sentinel error.
  • A function returns the sentinel value itself for its named failure.
  • Use errors.Is to test an error against a sentinel value.
  • A custom error type carries extra fields about a failure.
  • Use errors.As to copy a custom error value out of an error into a variable.
  • errors.Is answers which named failure happened.
  • errors.As answers what extra details a failure carries.

In this example, gate returns the sentinel for the value 4. errors.Is matches that sentinel, so main prints too small. Gate then returns a LimitError for 150, and errors.As copies it into limitErr. The last print uses the two fields and shows the value went 50 over the limit.

Note

A sentinel error is matched by its value, not by its message text.

Try it Yourself

Edit the code, then press Run.

Running is simulated · Go Playground

Exercise

Which call extracts a custom error type and its fields from an error?