Error handling in Go
We disallow use of error packages — including the stdlib errors
package — (enforced by a lint pass in CI) other than our internal github.com/sourcegraph/sourcegraph/lib/errors
package.
We also require the use of errors.Newf
over the use of fmt.Errorf
, which also constructs an error type. This is to ensure that each error constructed by Sourcegraph is tagged with a stack depth and allows redaction of content within user-visible strings.
errors.New
Use of Use this function to create an error value with a static message.
var ErrSomethingWentWrong = errors.New("something went wrong")
Idiomatic error messages in Go should start with a lowercase letter and should contain no trailing punctuation.
Generally, errors of this class should be created as a constant at the highest level possible (e.g., an unexported package constant). Such errors should be exported if direct comparison of error values should be allowed by a user.
Idiomatically, error constant values should always have a name of the format ErrX
(or errX
if package-private) and types that can be used as error
should have a name of the format XError
.
errors.Errorf
Use of Use this function to create an error value with non-static message.
return errors.Errorf("user %d does not exist", userID)
The formatting directives are the same as fmt.Sprintf
. The %w
formatting directive special cases error values. Prefer to use errors.Wrap
over this directive.
errors.Wrap
Use of Use this function to add additional data to an existing error value.
if err := myPkg.MyFunc(ctx); err != nil { return errors.Wrap(err, "myPkg.MyFunc") }
Wrap all errors being returned from a method if that error did not originate in the same package (except for standard library packages like os
or http
which have very obvious and distinct error messages) or the same struct (unless its not used from multiple callers). This produces error messages with a usable stacktrace.
errors.Is
Use of Use this function to determine if any cause of a given error value is equal to a target error value.
for _, filename := range filenames { f, err := os.Open(filename) if err != nil { if errors.Is(err, os.ErrNotExist) { // skip missing files continue } return err } defer f.Close() // ... }
errors.IsAny
Use of Use this function to determine if any cause of a given error is equal to at least one target error value.
if err := Some(ctx); err != nil { if errors.IsAny(err, context.DeadlineExceeded, context.Canceled) { // context error } else { // another type of error } }
As with the Is
function, the second argument to this function should generally be an error constant.
errors.HasType
Use of Use this function to determine if a given error has a particular type.
type NotFoundError struct { ID string } func (e *NotFoundError) Error() string { return fmt.Sprintf("Object `%d` not found", e.ID) } // Note: We use a pointer here because the receiver of the // target struct's Error method is a pointer. If the receiver // was a value type, we'd use a value type here as well. if errors.HasType(err, &NotFoundError{}) { // not found error } else { // another type of error }
This function should be preferred when comparing an error value with an error struct that may tag additional data (e.g., a failed opcode or the identifier of a missing object).
Note that using the Is
function here will fail to match any error that does not have equivalent fields. In the example above, Is
will only match error values where the value of the ID
field is the zero value.
errors.As
Use of Use this function to safely cast a given error value to a particular type. This should be preferred over using Go language-level type casing of an error value (e.g., err.(*MyType)
), which fails to unwrap errors.
type OpError struct { Op string } func (e OpError) Error() string { return fmt.Sprintf("Oops because of %s", e.Op) } // Note: We use a value type here because the receiver of the // target struct's Error method is a value type. If the receiver // was a pointer, we'd use a pointer here as well. var e OpError if errors.As(err, &e) { switch e.Op { // ... } } else { // ... }
This function can also be used with interface values.
func isRetryable(err error) bool { var e interface { Retryable() bool } if errors.As(err, &e) { return e.Retryable() } return false }
errors.Cause
Use of Use this function to remove all layers from a given error value and return only the root cause. This function should be used rarely as the Is
, HasType
, and As
functions cover the most common functionality.
fmt.Printf("Root error: %s\n", errors.Cause(err))
errors.MultiError
Use of MultiError
is an error interface that implements a "bag of errors". Typically, errors are chains: where error A causes error B, and so on, through the use of Wrap
. If you have tasks that run in parallel and return errors in tandem, for example, you may want a bag of errors instead.
To create a MultiError
, you can use Append
or CombineErrors
. A common paradigm is:
var err errors.MultiError for _, fn := range fnsThatReturnError { err = errors.Append(err, fn()) } return err
The MultiError
type:
- will be treated as a
nil
error if anAppend
orCombineErrors
only merges errors that arenil
- exposes errors within to introspection methods like
As
,Is
, etc. - prints all errors within in a multi-line list format on
(MultiError).Error()
- acts like a single error if the bag only contains a single error (notably for printing and introspection behaviours)
Check out the source code for the MultiError
implementation in lib/errors
.
Printing errors
Printing errors with most formatting directives like %s
, %v
, etc. will render an error by calling its Error()
implementation.
Errors created from the lib/errors
library, such as New
, Wrap
, etc. also carry additional details such as stack traces. This is exposed to integrations like Sentry, and can be rendered with the %+v
formatting directive.