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.

Use of errors.New

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.

Use of errors.Errorf

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.

Use of errors.Wrap

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.

Use of errors.Is

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()

	// ...
}

Use of errors.IsAny

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.

Use of errors.HasType

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.

Use of errors.As

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
}

Use of errors.Cause

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))

Use of errors.MultiError

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 an Append or CombineErrors only merges errors that are nil
  • 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.