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 or errors.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 use fmt.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, &notFound) {
    // 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 or errors.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")
}