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
}