Go Language Error Handling

From NovaOrdis Knowledge Base
Revision as of 23:12, 28 December 2023 by Ovidiu (talk | contribs) (→‎External)
Jump to navigation Jump to search

External

Internal

Overview

This article provides an overview of error handling in Go. It starts with error basics, where we explain that errors in Go are values, and functions return them as results. We continue by enumerating the ways error instances can be created and how the caller of a function that returns an error is supposed to handle it.

Error Basics

A big part of programming is how you handle errors. Errors in Go are values of the error type, not control structures. Java and Python's error handling mechanisms rely on program flow control structures, in the form of exceptions. Go is different in this respect, in that is has no exceptions, which implies that any error condition that you will encounter in a program is by definition not exceptional, but part of the normal flow of the program. The closest thing to exceptions in Go is the panic mechanism.

When an error condition occurs, the function in question must return an error value in addition to a result via its support for multiple return values. The error is typically returned as the first and only argument, if the function does not return anything else, or the last argument otherwise:

func someFunc(...) error {
  ...
  err := ...
  return err
}
func someFunc(...) (result SomeType, [...], error) {
  ...
  err := ...
  return ...,...,err
}

Note that the function might return a concrete type of an error instead of an interface, but this is usually a mistake. Returning the interface should be preferred.

If an error occurs, and the error value returned by the function is non-nil, all other values returned by the function must be ignored.

If no error occurs, the function must return nil as error value. In Go, nil means "no error".

The caller of the function is supposed to handle the error; an error returned by a function can be ignored if the caller assigns it to the blank identifier _, but this approach is discouraged. Ignoring an error leads in most cases to bad outcomes. Always handle the error.

It is the error implementation’s responsibility to summarize the context in the error message. When creating the error instance, the error string should start with a lowercase character, unless it begins with names that require capitalization. Error strings, unlike comments, should not end with punctuation marks. Keep error messages descriptive, yet compact. It should be easy to understand what exactly went wrong by reading the error message. Provide descriptive elements to help the operators of the program in figuring out what caused the error. For example, if a file cannot be opened because of lack of permission, the error message should be "open /some/path/myfile.txt: permission denied", not just "permission denied".

The error message can be introspected by invoking Error() on the error instance. However, the result of this introspection should not be used in the program logic. That output is aimed at humans operating the program, not code. The contents of that string belong in a log file or displayed on the screen. You should not try to change the behavior of the program as result of processing the error message. If you need more context carried within the error instance, look into custom error types.

Rob Pike's Go proverbs concerning errors: "Errors are just values" and "Don't just check errors, handle them gracefully". Also "error values in Go aren’t special, they are just values like any other, and so you have the entire language at your disposal".

Also see:

Go Style

Error Instance Creation

fmt.Errorf()

err := fmt.Errorf("some %s error", "blue")

The most common way to generate an error instance is fmt.Errorf(). fmt.Errorf() is equivalent to errors.New(), in that it produces a pointer to a new error instance, but it also allows formatting the error text using the capabilities of the fmt package. For this reason, fmt.Errorf() allows more flexibility and should be preferred over errors.New(). fmt.Errorf() invokes errors.New() internally. Each call to fmt.Errorf() returns a distinct error value even if the text is identical.

Error Wrapping

A new error instance can be created by wrapping an existing error with fmt.Errorf(). Error wrapping is an error handling idiom discussed in detail in Wrap the Error Instance below.

errors.New()

err := errors.New("some error")

The errors package New() function returns a pointer to a new error instance *errors.errorString that carries the given text. errorString is an unexported type of the errors package. Each call to New() returns a distinct error value even if the text is identical. The New() function does not have string formatting capabilities. The equivalent function fmt.Errorf() does.

Sentinel Errors

Sentinels are a special category of errors that are specifically intended to be identifiable using an equality test, which implies that only one error instance is used throughout the runtime. The name comes from the practice in computer programming of using a specific value to signify that no further processing is possible. In this case, we use specific values to signify an error, and we check for the presence of that value with the equality "==" operator. A sentinel error instance must have a unique identity, to make the evaluation with the equality test possible. An example of a sentinel error is io.EOF:

n, err := r.Read(buf)
if err == io.EOF {
 ...
}

The disadvantage of using sentinel errors is that they create a dependency between packages. For example, to check if an error is equal with io.EOF, you must import the io package. This particular example is not that bad, because io is quite common, but if this kind of coupling generalizes, it can become problematic. In consequence, avoid sentinel errors. There are a few cases when they are used in the standard library, but this is not a pattern you should emulate.

Custom Error Types

The error instances created with fmt.Error() and errors.New() carry context information in form of a detyped string. If we need to use context elements programmatically, during subsequent handling of the error, parsing the string is discouraged, as it can introduce brittleness. Custom error types is a better alternative.

Custom Error Types

Error Handling

There are three choices when handling an error returned by an invoked function:

  1. ignore it - not recommended
  2. handle the error, deal with the consequences and continue the flow, without returning the error the calling layer
  3. return the error to the calling layer

Deal with the Consequences and Continue the Flow

When calling a function returning an error, always handle the error first.

r, err := someFunc()
if err != nil {
  // handle the error condition, ignore the rest of the results
  ...
}

There's an if syntax that supports this error handling idiom:

if err := someFunc(); err != nil {
  // handle the error condition
  return
}
// handle success

For more details and advice on use see:

if syntax where the statement precedes expression

Return the Error to the Calling Layer

Handle the Error as Opaque and Return It Unmodified

func someFunc() error {
  ...
  err := someOtherFunc()
  if err != nil {
    return err
  }
  ...
}

Handling the error as opaque and just returning it as function result is the most flexible error handling strategy because it requires the least coupling between your code and the caller. It is opaque because while you know that an error occurred, you don't introspect the error. The problem with this approach is that the information where this error comes from is lost. Lost how? Update this section after researching stack traces.

Annotate the Error

The error instance returned from the underlying layer can be "annotated" with a string according to the following pattern:

err := someFunc()
if err != nil {
   return fmt.Errorf("some processing failed: %v", err)
}

The key is using fmt.Errorf() and the %v conversion character. The annotation process creates a new error instance whose message is modified to add extra context, in this case "some processing failed". Note that the original error instance is discarded, along with its unique identity, and not embedded in the new error value. This pattern is incompatible with sentinel error values and errors.Is() and type assertions because it is converting the original error instance to a string, merging it with another string, and then converting it back to an error. This breaks the type information that might be otherwise passed.

Wrap the Error Instance

Go error mechanism allows "wrapping" error instances into other error instances, while preserving the wrapped error identity. This pattern supports building an error tree that is useful in preserving context. Error wrapping is discussed in detail here:

Go Error Wrapping

The error Type and error Values

https://pkg.go.dev/builtin#error

The error is a pre-declared interface type, part of the universe block, and constitutes the conventional interface for representing an error condition, with the nil value representing no error.

type error interface {
  Error() string
}

The Error() method returns the string rendering of the error message. Errors in Go are instances that implement the error interface. Values of any type that implements the error interface can be used as error values and will be handled as error values by the Go runtime. error values are required to be more than just an error code, or a mere success/failure indicator.

The errors Package

https://pkg.go.dev/errors

The errors package contains functions for manipulating errors: creating new instances with errors.New(), checking for wrapped errors with errors.Is(), etc.

Stack Traces

Update above.

panic

panic

NotYetImplemented error

TODO