Go Language Error Handling: Difference between revisions

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


=Handling Errors=
=Handling Errors=
==Dealing with Consequences and Continuing the Flow==
==Returning the Unmodified Error==
==Returning the Unmodified Error==
==String-Wrapping==
==String-Wrapping==

Revision as of 23:33, 27 December 2023

Internal

Overview

This article provides a comprehensive 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 error type, returned by functions, not control structures. Java and Python use program flow control structures, in form of exceptions, as error handling mechanism. 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 unexpected condition occurs within a function, the function must return an error value, typically as the first and last argument, if the function does not return anything else, or the last argument otherwise:

func someFunc(...) error {
  ...
}
func someFunc(...) (result SomeType, error) {
  ...
}

Creating Error Instances

errors.New()

fmt.Errorf()

Custom Error Types

Wrapping

Sentinel Errors

Handling Errors

Dealing with Consequences and Continuing the Flow

Returning the Unmodified Error

String-Wrapping

Instance Wrapping

Error Good Practices

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".

The error Type

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.

The errors Package

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

Creating Error Instances

TO REFACTOR

Declaration

Initialization

err := errors.New("something went wrong")

Note that New() returns a pointer.

Another way to create errors is with fmt.Errorf():

err := fmt.Errorf("something")

fmt.Errorf() can also be used to wrap errors.

Idiomatic Error Handling

A common error handling pattern in Go involve functions returning several "good" values and an error value, as the last value in the series.

For valid invocations, the error return value is nil.

If the invocation caused an error, the error return value is not nil. If an error occurs the other values being returned by the function should be ignored.

Explicit error handling is some times cited as one of the strong points of Go.

f, err := os.Open("...")
if err != nil {
  fmt.Println(err) // will call the Error() method
  return
}
f, err := os.Open("...")
if err != nil {
  return errors.New("something went wrong")
}
var somethingWentWrong = errors.New("something went wrong")

f, err := os.Open("...")
if err != nil {
  return somethingWentWrong
}

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

if result, err := f(); err {
  // handle error
  ...
  return
}
// handle success
...

Use panics in truly exceptional cases.

Do not discard errors by using the blank identifier _ assignment, aways handle each error.

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

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.

Also see:

Go Style

Wrapping Errors

https://golang.org/pkg/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?

errors.As()

TODO

Panics

A panic indicates a programmer error (attempting to access an array index out of bounds, writing an uninitialized map, etc.). Panics can also be triggered with:

panic(<message>)

The panic can be caught on the stack outside the function that caused the panic, by executing recover(). Recover can be called in any upper function on the stack, or even if the function that triggers the panic, if the invocation is made with defer.

The recover() built-in function allows a program to manage behavior of a panicking goroutine. Executing a call to recover inside a deferred function, but not any function called by it, stops the panicking sequence by restoring normal execution and retrieves the error value passed to the call of panic. If recover is called outside the deferred function it will not stop a panicking sequence. In this case, or when the goroutine is not panicking, or if the argument supplied to panic was nil, recover returns nil. Thus the return value from recover reports whether the goroutine is panicking.

The error Interface

Error Patterns

Sentinel Errors

Sentinel errors is a form of Go error handling. The name descends 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.

Example of sentinel error: io.EOF:

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

Sentinel error 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.

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.

Opaque Errors

This 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 have the ability to see inside the error.

err := authenticate(r.User)
if err != nil {
  return err
}

The problem with this approach is that it loses the information where this error comes from.

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.

Wrapping Errors

https://dave.cheney.net/2016/06/12/stack-traces-and-the-errors-package

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.