Go Language Error Handling: Difference between revisions
Line 121: | Line 121: | ||
=<span id='Handling_Errors'></span>Error Handling= | =<span id='Handling_Errors'></span>Error Handling= | ||
Handling an error means inspecting the error value and making a decision. Always handle the error. When handling the error, [[#Only_Handle_an_Error_Once|only handle it once]]. Making more than one decision in response to a single error is bad. This is an [[#Handling_the_Error_Twice_-_Bad|example of handling an error twice]]. | 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_an_Error_Once|only handle it once]]. Making more than one decision in response to a single error is bad. This is an [[#Handling_the_Error_Twice_-_Bad|example of handling an error twice]]. | ||
There are three choices when handling an error returned by an invoked function: | There are three choices when handling an error returned by an invoked function: |
Revision as of 01:28, 31 December 2023
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
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, which usually indicates an unwanted situation, 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. 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 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.
When exported, errors are part of your package's public API, treat them with as much care as you would any other part of your public API.
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.
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")
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.
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. This is an example of handling an error twice.
There are three choices when handling an error returned by an invoked function:
- ignore it by assigning to the blank identifier
_
or "handling" it in an empty block - not recommended - 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.
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:
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. Error wrapping should be preferred over error annotation when it comes to provide additional context around an error.
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 context to an error, and it should be the preferred pattern to use when we want to provide additional context to an error. 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.
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
Update above.
panic
NotYetImplemented error
For a typical implementation and usage, see Sentinel Errors.