mdawar.dev

A blog about programming, Web development, Open Source, Linux and DevOps.

Go - Errors

Error states are represented with error values.

Error Guidelines

  • The nil value represents no error (The zero value for the interface type)
  • A function’s error value should be the last return value
  • On error a function should return zero values for its other return parameters
  • Error messages should not be capitalized
  • Error messages should not end with punctuation or a newline
  • Error variables are named starting with err or Err

Error Type

The error type is a built-in interface.

go
// Any type that defines an `Error()` method implements the `error` interface.
// The `fmt` package formats an error value by calling its `Error()` method.
type error interface {
Error() string
}

Error Handling

Errors are checked by inspecting a function’s error return value, if the error value is not nil it means an error has occured.

go
// A function that might return an error.
// A nil value is returned on success.
err := SomeFunction()
// A non-nil error denotes failure.
// Check for errors by testing whether the value is not equal to nil.
if err != nil {
// Handle the error
}

Creating Errors

Create an error for a given error message:

go
import "errors"
// `errors.New` returns a pointer to a struct that holds the error message.
err := errors.New("connection error")
// Each call to `errors.New` returns a distinct error value
// even if the text is identical.
errors.New("not found") == errors.New("not found") // false

Create an error with a formatted error message:

go
import "fmt"
// `fmt.Errorf` is a wrapper for `errors.New` with string formatting.
err := fmt.Errorf("user %s (ID %d) was not found", name, id)

Equivalent to:

go
import (
"errors"
"fmt"
)
err := errors.New(fmt.Sprintf("user %s (ID %d) was not found", name, id))

Custom Error Types

Since error is an interface, any value that has a method Error() string can be used as an error.

Custom error types can be used to add additional details to the error to be inspected when needed.

go
// Define a struct to hold the error message with the additional data.
// This error type holds an error message with an integer code.
type CustomError struct {
Msg string
Code int
}
// This type implements the error interface by having an `Error` method.
func (e *CustomError) Error() string {
return fmt.Sprintf("error %d: %s", e.Code, e.Msg)
}

Usage:

go
// A function returning a custom error.
func doSomething() error {
// Create an error with additional data.
// Return a pointer because Error() is implemented on the pointer receiver.
return &CustomError{
Code: 404,
Msg: "not found",
}
}
func main() {
err := doSomething()
// The error can be handled just like any other error.
if err != nil {
fmt.Println(err)
}
}

To access the error’s fields and methods, a type assertion can be used (Not recommended):

go
func main() {
err := doSomething()
// Use a type assertion to access the error details.
if err, ok := err.(*CustomError); ok {
fmt.Println(err.Code, err.Msg)
}
}

Using errors.As is the preferred way of accessing the error’s fields and methods, as it works with wrapped errors:

go
func main() {
err := doSomething()
// Variable to hold the error value.
var customErr *CustomError
if errors.As(err, &customErr) {
fmt.Println(customErr.Code, customErr.Msg)
}
}

Expected Errors

Expected errors or sentinel errors are useful for providing errors to be checked and handled in a specific way.

go
package example
import "errors"
// An error to be handled in a specific way.
// By convention the error name starts with `Err`.
var ErrNotFound = errors.New("not found")

Errors defined in this way can be compared directly using a simple equality check (Not recommended):

go
err := SomeFunction()
// This check will not succeed if the error is wrapped.
if err == example.ErrNotFound {
// Handle the error
}

Using errors.Is is the preferred way of checking for a specific error:

go
err := SomeFunction()
// This check will succeed even if the error is wrapped.
if errors.Is(err, example.ErrNotFound) {
// Handle the error
}

Wrapping Errors

A wrapped error is simply an error that contains another error.

Wrapping an error is the process of creating a new error that includes the original error along with additional context.

A series of wrapped errors is called an error chain.

An error wraps another error if its type has an Unwrap() method that returns an error or []error value.

Error wrapping is done using fmt.Errorf with the %w verb:

go
err := SomeFunction()
if err != nil {
// Create a new error that wraps (includes) the original error
// with additional context about the failure.
// To create an error that contains the message from the original error
// without wrapping it, use the %v verb instead of %w.
return fmt.Errorf("failed to execute SomeFunction: %w", err)
}

Unwrapping the error is done using errors.Unwrap (Usually it’s not used directly, errors.Is and errors.As are used instead):

go
err := SomeFunction()
// `errors.Unwrap` returns nil if the error does not wrap any other error.
if wrappedErr := errors.Unwrap(err); wrappedErr != nil {
fmt.Println(wrappedErr) // Handle the original error
}

When wrapping an error, a new error is created that includes the original error:

go
// The error type created by the `fmt.Errorf` function.
type wrapError struct {
msg string // Error message
err error // Wrapped (original) error
}
func (e *wrapError) Error() string {
return e.msg
}
// `Unwrap` returns the original wrapped error.
func (e *wrapError) Unwrap() error {
return e.err
}

When wrapping errors with a custom error type it needs to implement an Unwrap() method:

go
type CustomError struct {
Msg string
Code int
Err error // Wrapped error
}
func (e *CustomError) Error() string {
return e.Msg
}
// Return the wrapped error with an `Unwrap` method.
// Called by `errors.Unwrap` to retrieve the wrapped error.
func (e *CustomError) Unwrap() error {
return e.Err
}
func doSomething() error {
err := SomeFunction()
// Use `CustomError` to wrap other errors.
if err != nil {
return &CustomError{
Msg: "doSomething failed",
Code: 1000,
Err: err // The original error
}
}
return nil
}

Checking Error Values

errors.Is is used to check for a specific error value or type.

go
err := SomeFunction()
if errors.Is(err, ErrSomeError) {
// Handle the error
}

errors.As is used to perform a type assertion on an error and access its fields and methods.

go
err := SomeFunction()
// Declare a variable of the error type to check.
var customErr *CustomError
// Pass a pointer to the variable to `errors.As` to be assigned.
if errors.As(err, &customErr) {
// Access the error's fields and methods.
fmt.Println(customErr.Code)
}

A pointer to an interface can be used with errors.As:

go
err := SomeFunction()
// Using an anonymous interface.
// Check for errors with a `Code()` method.
var errWithCode interface {
Code() int
}
// Pass the pointer to the interface.
if errors.As(err, &errWithCode) {
fmt.Println(errWithCode.Code()) // Use the interface value
}