There are three main options for propagating errors if a call fails:
- return the original error as-is
- add context with
fmt.Errorf
and the%w
verb - add context with
fmt.Errorf
and the%v
verb
Return the original error as-is if there is no additional context to add. This maintains the original error type and message. This is well suited for cases when the underlying error message has sufficient information to track down where it came from.
Otherwise, add context to the error message where possible so that instead of a vague error such as "connection refused", you get more useful errors such as "call service foo: connection refused".
Use fmt.Errorf
to add context to your errors,
picking between the %w
or %v
verbs
based on whether the caller should be able to
match and extract the underlying cause.
- Use
%w
if the caller should have access to the underlying error. This is a good default for most wrapped errors, but be aware that callers may begin to rely on this behavior. So for cases where the wrapped error is a knownvar
or type, document and test it as part of your function's contract. - Use
%v
to obfuscate the underlying error. Callers will be unable to match it, but you can switch to%w
in the future if needed.
When adding context to returned errors, keep the context succinct by avoiding phrases like "failed to", which state the obvious and pile up as the error percolates up through the stack:
Bad | Good |
---|---|
s, err := store.New()
if err != nil {
return fmt.Errorf(
"failed to create new store: %w", err)
} |
s, err := store.New()
if err != nil {
return fmt.Errorf(
"new store: %w", err)
} |
|
|
However once the error is sent to another system, it should be clear the
message is an error (e.g. an err
tag or "Failed" prefix in logs).