Go Language Error Handling: Difference between revisions
Line 24: | Line 24: | ||
A big part of programming is how you handle errors. Errors in Go are values of the <code>[[#error|error]]</code> type, not control structures. Java and Python's error handling mechanisms rely on exceptions, which are program flow control structures. Go is different, 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. This approach makes the statement that error handling is important, and as we develop our programs, we should give our error paths the same attention we give our algorithms. The closest thing to exceptions in Go is the [[Go panic|panic]] mechanism. | A big part of programming is how you handle errors. Errors in Go are values of the <code>[[#error|error]]</code> type, not control structures. Java and Python's error handling mechanisms rely on exceptions, which are program flow control structures. Go is different, 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. This approach makes the statement that error handling is important, and as we develop our programs, we should give our error paths the same attention we give our algorithms. The closest thing to exceptions in Go is the [[Go panic|panic]] mechanism. | ||
When an error condition occurs, which usually indicates that the system has entered in a state in which it cannot fulfill an operation that a user explicitly or implicitly requested, the function in question must return an error value in addition to the 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: | When an error condition occurs, which usually indicates that the system has entered in a state in which it cannot fulfill an operation that a user explicitly or implicitly requested, the function in question must return an error value in addition to the result via its [[Go_Functions#Multiple_Return_Values|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: | ||
<syntaxhighlight lang='go'> | <syntaxhighlight lang='go'> |
Latest revision as of 17:15, 14 August 2024
External
- https://go.dev/blog/error-handling-and-go
- https://dave.cheney.net/2012/01/18/why-go-gets-exceptions-right
- https://dave.cheney.net/2014/11/04/error-handling-vs-exceptions-redux
- https://dave.cheney.net/2015/01/26/errors-and-exceptions-redux
- https://gabrieltanner.org/blog/golang-error-handling-definitive-guide
Internal
TODO
- Investigate github.com/hashicorp/go-multierror and morph the learnings into this article.
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. We also discuss how to handle errors in concurrent programming.
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 exceptions, which are program flow control structures. Go is different, 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. This approach makes the statement that error handling is important, and as we develop our programs, we should give our error paths the same attention we give our algorithms. The closest thing to exceptions in Go is the panic mechanism.
When an error condition occurs, which usually indicates that the system has entered in a state in which it cannot fulfill an operation that a user explicitly or implicitly requested, the function in question must return an error value in addition to the 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 the error
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".
When the error is created, it is the error implementation’s responsibility to summarize the context in the error message. 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. Error messages should be descriptive, yet compact. It should be easy to understand what exactly went wrong by reading the error message. Descriptive elements in the message help the operators of the program to figure 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 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. Handling an error means inspecting the error value and making a decision. Always handle the error. When handling the error, only handle it once.
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.
A valid pattern to handle an error is augmenting the error with more context and returning it to the upper layer. When possible, simply return the error unchanged. If you must add more context, prefer error wrapping over annotation.
Another error handling pattern is built around the fact that all errors belong to one of two categories: bugs and known edge cases (disk full, network failure, etc.). The pattern requires to represent the known edge cases as "well-formed" errors and declare them as part of our component API. All other errors are "raw errors" and represent bugs. When exported, errors become part of your package's public API and must be treated with as much care as you would any other part of your public API. This pattern is described here: Raw Errors and Well-Formed Errors.
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:
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 and it should be preferred over errors.New()
.
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 or with errors.Is()
(preferred). 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 typical way to declare and use a sentinel error:
. ├── internal │ ├── errs │ │ ├── errs.go
The package name is designed to avoid clashing with the built-in type error
, with the errors
package name and with the commonly used variable name err
.
package errs
...
var NotYetImplemented = fmt.Errorf("not yet implemented")
var NotFound = fmt.Errorf("not found")
If it is not practical to create a new errs
package and the errors must be declared in an existing package, it is OK to postfix the error instance name with Error
:
package something
...
var NotYetImplementedError = fmt.Errorf("not yet implemented")
var NotFoundError = fmt.Errorf("not found")
To check for occurrence, prefer errors.Is()
over type assertions. errors.Is()
is designed to work with both individual errors and wrapped errors.
func someFunc() error {
return errs.NotYetImplemented
}
...
err := someFunc()
if errors.Is(err, errs.NotYetImplemented) {
fmt.Printf("someFunc() is not yet implemented")
}
There are a few disadvantages to using sentinel errors.
The most serious is that the sentinel errors 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. Using sentinel errors as part of the internal
packages of a module and not exporting them as part of the public interface of the module is probably OK.
Another is that an attempt to annotate the sentinel error to provide more context would cause returning a different error would break the equality check. This is a limitation that can be worked around by wrapping the sentinel error inside another error and using errors.Is()
or errors.As()
to check for it.
In consequence, it improbably wise to 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 Variable Naming
Go lore recommends that the error variable names should be of the form ErrSomething
:
var ErrSomething = fmt.Error("something")
The above documentation uses SomethingError
format. Reconcile.
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, but it also comes with disadvantages, which will be discussed here:
Error Handling
Handling an error means inspecting the error value and making a decision. Always handle the error, never ignore it. When handling the error, only handle it once. Making more than one decision in response to a single error is bad. Logging an error is handling the error.
There are three choices when handling an error:
- ignore it by assigning to the blank identifier
_
or "handling" it in an empty block - not recommended. However, if ignoring the error makes sense in a specific instance, also add a comment that explains why we are ignoring the error, to confirm to the code reader that ignoring the error is intentional and not a mistake. - handle the error, deal with the consequences and continue the flow, without returning the error the calling layer
- 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
...
}
decision. Always handle the error. When handling the error, only handle it once.
The "if
with initialization statement" syntax 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:
Return the Error to the Calling Layer
The error can be returned to the calling layer unmodified, it can be annotated while being morphed into a different error, or it can be wrapped into a new error while preserving its identity. Different situations call for different approaches, as discussed below.
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. This approach is appropriate when there is no additional context to be added to the error, and also we do not want to mark the error as a more specific type. 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 transforms the original error by creating 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 converts the original error instance to a string, it merges it with another string, and then it converts the string back to a new error. This breaks the type information that might be otherwise passed. Error wrapping should be preferred over error annotation when it comes to provide additional context around an error.
This approach is appropriate when we need to add more context to an error returned by the lower layer, while we do not care about preserving the error identity.
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 providing deep context to an error, and it should be the preferred pattern to use when we want to provide additional context to an error.
There are two situation when wrapping is an appropriate approach: when providing more context to an error while preserving the error history, and also when we want to mark an error as a specific type. Error wrapping is discussed in detail here:
Only Handle an Error Once
An error must be handled once. This is an example of handling an error twice:
result, err := someFunction()
if err != nil {
// handling it once: we annotate the error and then send it to the log file
log.Println("something went wrong: ", err)
// handling it the second time: we send the unannotated error to the caller
return err
}
We log the annotated error and return the unannotated error to the caller, who possibly will log it, and return it, all the way back up to the top of the program. In the end, we get a stack of duplicate lines in the log file, and at the top of the program we get the original error without any context. Having more than one log line for a single error is a problem, because it makes debugging harder.
Logging an error is handling the error. Logging the error and returning the same error is handling the error twice. We should log or return, never both.
Concurrent Programming Error Handling
This section discusses error handling in the context of concurrent programming with goroutines and channels.
In case of concurrent processing, the concurrent functions should send their errors to another part of the program that has complete information about the state of the program, and that can make more informed decision on what to do. A common pattern to implement this:
- Introduces a type that encapsulates the valid result and the error.
- Return the instances of that type on the channel that is supposed to carry the valid result.
- The reader on the channel, which is supposed to know what to do with the result, it is in a better position to decide what to do in case of error, in a richer context.
The instances of the <valid-result, error> type represent the complete set of possible outcomes of the concurrent code that produces the results, and allows us to separate the concern of handing the errors from that of producing the results. The goroutine that spawned the producer goroutine has a more complete context and can make more intelligent decisions about what to do with the errors.
The error Type and error Values
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. When printing the error using methods like fmt.Println()
the Error()
method is automatically invoked.
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
The errors
package contains functions for manipulating errors: creating new instances with errors.New()
, checking for wrapped errors with errors.Is()
, errors.As()
, etc.
Stack Traces
A stack trace in form of a []byte
is generated with:
import "runtime/debug"
...
ba := debug.Stack()
To print the stack trace at stderr:
import "runtime/debug"
debug.PrintStack()
Update above.
Errors and Logging
err := fmt.Errorf("this is an error")
log.Printf("%v\n", err)
prints:
2023/12/30 19:02:06 this is an error
How do I display stack traces?
panic
NotYetImplemented error
For a typical implementation and usage, see Sentinel Errors.