Golang Error Handling Specification
Error Types
There are few options for declaring errors. Before choosing the option that best fits your use case, consider the following:
- Does the caller need to match the error to handle it? If so, we must support
errors.Is
orerrors.As
functions by declaring top-level error variables or custom types. - Is the error message a static string or a dynamic string requiring contextual information? For static strings, we can use
errors.New
, but for the latter, we must usefmt.Errorf
or a custom error type. - Are we passing new errors returned by downstream functions? If so, refer to the error wrapping section.
Error Match? | Error Message | Guidance |
---|---|---|
No | static | errors.New |
No | dynamic | fmt.Errorf |
Yes | static | top-level var with errors.New |
Yes | dynamic | custom error type |
For example, use errors.New
to represent errors with static strings. If the caller needs to match and handle this error, export it as a variable to support matching with errors.Is
.
No Error Matching
// package foo
func Open() error {
return errors.New("could not open")
}
// package bar
if err := foo.Open(); err != nil {
// Can't handle the error.
panic("unknown error")
}
Error Matching
// package foo
var ErrCouldNotOpen = errors.New("could not open")
func Open() error {
return ErrCouldNotOpen
}
// package bar
if err := foo.Open(); err != nil {
if errors.Is(err, foo.ErrCouldNotOpen) {
// handle the error
} else {
panic("unknown error")
}
}
For errors with dynamic strings, use fmt.Errorf
if the caller does not need to match it. If the caller indeed needs to match it, then use a custom error
.
No Error Matching
// package foo
func Open(file string) error {
return fmt.Errorf("file %q not found", file)
}
// package bar
if err := foo.Open("testfile.txt"); err != nil {
// Can't handle the error.
panic("unknown error")
}
Error Matching
// package foo
type NotFoundError struct {
File string
}
func (e *NotFoundError) Error() string {
return fmt.Sprintf("file %q not found", e.File)
}
func Open(file string) error {
return &NotFoundError{File: file}
}
// package bar
if err := foo.Open("testfile.txt"); err != nil {
var notFound *NotFoundError
if errors.As(err, ¬Found) {
// handle the error
} else {
panic("unknown error")
}
}
Note that if you export error variables or types from a package, they become part of the package’s public API.
Error Wrapping
When an error occurs while calling another method, there are usually three ways to handle it:
- Return the original error as it is.
- Use
fmt.Errorf
with%w
to add context to the error and then return it. - Use
fmt.Errorf
with%v
to add context to the error and then return it.
If there is no additional context to add, return the original error as it is. This will preserve the original error type and message. This is particularly suitable when the underlying error message contains enough information to trace where the error originated from.
Otherwise, add context to the error message as much as possible so that ambiguous errors like “connection refused” do not occur. Instead, you will receive more useful errors, such as “calling service foo: connection refused”.
Use fmt.Errorf
to add context to your errors and choose between %w
or %v
verbs based on whether the caller should be able to match and extract the root cause.
- Use
%w
if the caller should have access to the underlying error. This is a good default for most wrapping errors, but be aware that the caller may begin to rely on this behavior. Therefore, for wrapping errors that are known variables or types, record and test them as part of the function contract. - Use
%v
to obfuscate the underlying error. The caller will not be able to match it, but you can switch to%w
in the future if needed.
When adding context to the returned error, avoid using phrases like “failed to” to keep the context concise. When the error permeates through the stack, it will be stacked up layer by layer:
Not Recommended:
s, err := store.New()
if err != nil {
return fmt.Errorf(
"failed to create new store: %w", err)
}
// failed to x: failed to y: failed to create new store: the error
Recommended:
s, err := store.New()
if err != nil {
return fmt.Errorf(
"new store: %w", err)
}
// x: y: new store: the error
However, once the error is sent to another system, it should be clear that the message is an error (e.g., an “err” tag or a “Failed” prefix in the logs).
Incorrect Naming
For error values stored as global variables, use the prefix Err
or err
based on whether they are exported. Please refer to the guidelines. For unexported top-level constants and variables, use an underscore (_) as a prefix.
var (
// Export the following two errors so that users of this package can match them with errors.Is.
ErrBrokenLink = errors.New("link is broken")
ErrCouldNotOpen = errors.New("could not open")
// This error is not exported because we do not want it to be part of our public API. We may still use it within the package with errors.
errNotFound = errors.New("not found")
)
For custom error types, use the suffix Error
.
// Similarly, this error is exported so that users of this package can match it with errors.As.
type NotFoundError struct {
File string
}
func (e *NotFoundError) Error() string {
return fmt.Sprintf("file %q not found", e.File)
}
// This error is not exported because we do not want it to be part of the public API. We can still use it within a package with errors.As.
type resolveError struct {
Path string
}
func (e *resolveError) Error() string {
return fmt.Sprintf("resolve %q", e.Path)
}
Handling Errors
When the caller receives an error from the callee, it can handle the error in various ways based on the understanding of the error.
This includes but is not limited to:
- Matching the error with
errors.Is
orerrors.As
if the callee has agreed on a specific error definition, and handling the branching in different ways - Logging the error and gracefully degrading if the error is recoverable
- Returning a well-defined error if it represents a domain-specific failure condition
- Returning the error, whether it is wrapped or verbatim
Regardless of how the caller handles the error, it should typically handle each error only once. For example, the caller should not log the error and then return it, as its caller might also handle the error.
For instance, consider the following scenarios:
Bad: Logging the error and returning it
Other callers further up the stack may take similar actions on this error. This would create a lot of noise in the application logs with little benefit.
u, err := getUser(id)
if err != nil {
// BAD: See description
log.Printf("Could not get user %q: %v", id, err)
return err
}
Good: Wrapping the error and returning it
The errors further up the stack will handle this error. Using %w
ensures that they can match the error with errors.Is
or errors.As
if relevant.
u, err := getUser(id)
if err != nil {
return fmt.Errorf("get user %q: %w", id, err)
}
Good: Logging the error and gracefully degrading
If the operation is not absolutely necessary, we can provide graceful degradation by recovering from it without interrupting the experience.
if err := emitMetrics(); err != nil {
// Failure to write metrics should not
// break the application.
log.Printf("Could not emit metrics: %v", err)
}
Good: Matching the error and gracefully degrading appropriately
If the callee has defined a specific error in its agreement and the failure is recoverable, match that error case and gracefully degrade. For all other cases, wrap the error and return it. The errors further up the stack will handle other errors.
tz, err := getUserTimeZone(id)
if err != nil {
if errors.Is(err, ErrUserNotFound) {
// User doesn't exist. Use UTC.
tz = time.UTC
} else {
return fmt.Errorf("get user %q: %w", id, err)
}
}
Handling Assertion Failures
Type assertions will panic with a single return value in case of an incorrect type detection. Therefore, always use the “comma, ok” idiom.
Not Recommended:
t := i.(string)
Recommended:
t, ok := i.(string)
if !ok {
// Handle the error gracefully
}
Avoid using panic
Code running in production environment must avoid panic. Panic is the primary source of cascading failures. If an error occurs, the function must return the error and allow the caller to decide how to handle it.
Not recommended:
func run(args []string) {
if len(args) == 0 {
panic("an argument is required")
}
// ...
}
func main() {
run(os.Args[1:])
}
Recommended:
func run(args []string) error {
if len(args) == 0 {
return errors.New("an argument is required")
}
// ...
return nil
}
func main() {
if err := run(os.Args[1:]); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
panic/recover is not an error handling strategy. It must only panic when an unrecoverable event (e.g., nil reference) occurs. An exception is during program initialization: situations that would cause a program to panic should be handled during program startup.
var _statusTemplate = template.Must(template.New("name").Parse("_statusHTML"))
Even in test code, it is preferable to use t.Fatal
or t.FailNow
instead of panic to ensure that failures are marked.
Not recommended:
// func TestFoo(t *testing.T)
f, err := os.CreateTemp("", "test")
if err != nil {
panic("failed to set up test")
}
Recommended:
// func TestFoo(t *testing.T)
f, err := os.CreateTemp("", "test")
if err != nil {
t.Fatal("failed to set up test")
}