Error Handling in Go

TL;DR

There are some things we can consider when we deal with errors in Go:

Defining Sentinel Error

To define a sentinel error, we can use fmt.Errorf or errors.New to create a new variable. In fact, if fmt.Errorf is called without the %w verb, it will call errors.New, like the code below:

var ErrSomethingIsWrong = fmt.Errorf("There is something wrong") // This calls errors.New

To check if an error is a certain sentinel error, use errors.Is. For example, suppose we're reading from a configuration file, and we want to do something if the configuration file doesn't exist, we can use errors.Is to check if the error is fs.ErrNotExist:

_, err := os.Open("path/to/configuration/file")
if errors.Is(err, fs.ErrNotExist) {
	fmt.Println(err)
}

We can wrap an error to make a new error with fmt.Errorf and the %w verb. In doing so, it kind of gives us a feeling that one error is derived from the other. For example, in below code, an ErrConfigurationNotExist could also be considered as a fs.ErrNotExist error.

var ErrConfigurationNotExist = fmt.Errorf("Configuration file not exist: %w", fs.ErrNotExist)

func foo() {
    err := ...

    // If the err is ErrConfigurationNotExist, then both of the following lines will print true
    fmt.Println(errors.Is(err, ErrConfigurationNotExist))
    fmt.Println(errors.Is(err, fs.ErrNotExist))
}

Custom Error Type

A custom error type is needed when we want more information other than just an error message, like json.SyntaxError:

type SyntaxError struct {
	msg    string // description of error
	Offset int64  // error occurred after reading Offset bytes
}

To check if an error is a certain type, we use errors.As:

err := ...

var syntaxError *json.SyntaxError
if errors.As(err, &syntaxError) {
    fmt.Println(syntaxError)
}

In above code, we pass the address of the variable syntaxError to errors.As. If errors.As returns true, the variable will be populated, and we know a SyntaxError appeared.

Third-party Errors

Normally when we call a third-party package, we deal with the error returned immediately. However, there are times when we want to just return the error and handle it somewhere else. To do so, we rely on the package to have either sentinel error or a custom error type.

If a third-party package doesn't provide a sentinel error or error type, we can create a custom error type to wrap the error and implement Unwrap to expose them.

type ErrThirdParty struct {
	err error
}

func (e *ErrThirdParty) Error() string {
	return "thirdParty error: " + e.err.Error()
}

func (e *ErrThirdParty) Unwrap() error {
	return e.err
}

func foo() error {
	err := thirdParty.FunctionCall()
	if err != nil {
		return &ErrThirdParty{err: err}
	}
	return nil
}

This is a custom error type, so we still use errors.As to check it.

The downside of this approach is that if one forgets to wrap the error, then we won't be able to catch that.

Further Readings