Go Language Error Handling: Difference between revisions

From NovaOrdis Knowledge Base
Jump to navigation Jump to search
Line 206: Line 206:


=Error Patterns=
=Error Patterns=
==Error Types==
Error types is another form of Go error handling. An error type is a type created by implementing the <code>error</code> interface.
Example:
<syntaxhighlight lang='go'>
... gocon-spring-2016.pdf
</syntaxhighlight>
and then use [[Go Type Assertions#Overview|type assertions]]
<syntaxhighlight lang='go'>
err := something()

switch err := err.(type) {
 case nil:

// call succeeded, nothing to do
 case *MyError:

fmt.Println(“error occurred on line:”, err.Line)
 default:

}
</syntaxhighlight>
An improvement of error types over error values is their ability to wrap an underlying error in a new type to provide more context. See <code>os.PathError</code> for an example.
The problem with error types is that they must be made public, so the caller can use a [[Go_Type_Assertions#Type_Assertion|type assertion]] or a [[Go_Type_Assertions#Type_Switch|type switch]]. If your code implements an interface whose contract requires a specific error type, all implementors of that interface need to depend on the package that defines the error type. This knowledge of a package's types creates a strong coupling with the caller, making for a brittle API.
Avoid error types, at least avoid making them part of the public API.
=Annotating Errors=
=Annotating Errors=



Revision as of 03:12, 28 December 2023

Internal

Overview

This article provides a review 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 listing the ways error instances can be created and how the caller of a function that generated an error is supposed to handle them. We conclude with a few good practices collected from various sources.

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 have error handling mechanisms relying on program flow control structures, in the form of exceptions. Go is different in this respect, in that is has no exceptions and it 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.

If some situation that warrants generation of an error occurs, the function in question must return an error value, typically 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
}

If an error occurs, and the error value 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.

Errors can be ignored if the caller assigns them to the blank identifier _, but this approach is discouraged. Ignoring an error leads in most cases to bad outcomes. Always handle the error. Error strings should start with a lowercase character, unless they begin with names that require capitalization. Error strings, unlike comments, should not end with punctuation marks. Keep errors descriptive, yet compact. It should be easy to understand what exactly went wrong by reading the error message.

Rob Pike's Go proverbs concerning errors: "Errors are just values" and "Don't just check errors, handle them gracefully".

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 an error instance pointer, but that 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(). Each call to fmt.Errorf() returns a distinct error value even if the text is identical.

errors.New()

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

The errors package New() function returns a pointer to an error instance *errors.errorString that carries the given text. 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.

Error Wrapping

A new error instance can be created by wrapping an existing error. See Error Instance Wrapping below.

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

Error types is another form of Go error handling. An error type is a type created by implementing the error interface.

Example:

... gocon-spring-2016.pdf

and then use type assertions

err := something()
switch err := err.(type) {case nil:
// call succeeded, nothing to do
 case *MyError:

fmt.Println(error occurred on line:, err.Line)default:
}

An improvement of error types over error values is their ability to wrap an underlying error in a new type to provide more context. See os.PathError for an example.

The problem with error types is that they must be made public, so the caller can use a type assertion or a type switch. If your code implements an interface whose contract requires a specific error type, all implementors of that interface need to depend on the package that defines the error type. This knowledge of a package's types creates a strong coupling with the caller, making for a brittle API.

Avoid error types, at least avoid making them part of the public API.

Error Handling

While handling an error returned by the called function, there are two choices: deal with the consequences and continue the flow, without returning the error, or 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
  ...
}

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 It as Opaque and Return the Unmodified Error

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. Update this section after researching stack traces.

String-Wrapping

Error Instance Wrapping

Define error wrapping and why is necessary.

Done with fmt.Errorf()

Define a wrapped error. Compare fmt.Errorf() to errors.Unwrap().

Existing errors can be wrapped in outer errors, which may add information relevant to the context that caught the error being processed. However, wrapping error is more than concatenating strings.

The conventional way of wrapping errors in Go is to use the %w formatting clause with fmt.Errorf():

if err != nil {
  return fmt.Errorf("additional information: %w", err)
}

The wrapped error message, assuming that the original error is "initial information", with be:

additional information: initial information

Checking for Wrapped Errors

A wrapped error can be identified in an enclosing error with the errors.Is(<outer_error>, <sought_for_error>) function:

var BlueError = errors.New("some information")
var GreenError = errors.New("some information")

...

// wrap the error in an outer error
outerError := fmt.Errorf("addtional info: %w", BlueError)

if errors.Is(outerError, BlueError) {
  fmt.Println("found blue error")
}

if errors.Is(outerError, GreenError) {
  fmt.Println("found green error")
}

BlueError is correctly identified, even though both BlueError and GreenError carry the same string. How?

.

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 prints 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 New(), checking for wrapped errors with Is(), etc.

Stack Traces

Update above.

panic

panic

TO REFACTOR

errors.As()

TODO

Error Patterns

Annotating Errors

err := authenticate(r.User)
if err != nil {
   return fmt.Errorf("authenticate failed: %v", err)
}


Add context to the error path with fmt.Errorf. This pattern is incompatible with sentinel error values and type assertions because it is converting the error 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.

The Output of error.Error()

You should never inspect the output of the error.Error() method. 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 by inspecting it.