Skip to content

tozd/go-errors

Repository files navigation

Errors with a stack trace

pkg.go.dev Go Report Card pipeline status coverage report

A Go package providing errors with a stack trace and optional structured details.

Features:

  • Based of github.com/pkg/errors with compatible API, addressing many its open issues. In can be used as a drop-in replacement and even mixed with github.com/pkg/errors.
  • Uses standard error wrapping (available since Go 1.13) and wrapping of multiple errors (available since Go 1.20).
  • All errors expose information through simple interfaces which do not use custom types. This makes errors work without a dependency on this package. They work even across versions of this package.
  • It is interoperable with other popular errors packages and can be used in projects where they are mixed to unify their stack traces and formatting.
  • Provides errors.Errorf which supports %w format verb to both wrap and record a stack trace at the same time (if not already recorded).
  • Provides errors.E type to be used instead of standard error to annotate which functions return errors with a stack trace and details.
  • Clearly defines what are differences and expected use cases for:
    • errors.Errorf: creating a new error and recording a stack trace, optionally wrapping an existing error
    • errors.WithStack: adding a stack trace to an error without one
    • errors.WithMessage: adding a prefix to the error message
    • errors.Wrap and errors.WrapWith: creating a new error but recording its cause
    • errors.Join: joining multiple errors which happened during execution (e.g., additional errors which happened during cleanup)
    • errors.Prefix: combine multiple errors by prefixing an error with base errors
  • Provides errors.Base function to create errors without a stack trace to be used as base errors for errors.Is and errors.As.
  • Differentiates between wrapping and recording a cause: errors.Wrap and errors.WrapWith record a cause, while other functions are error transformers, wrapping the original.
  • Makes sure a stack trace is not recorded multiple times unnecessarily.
  • Provides optional details map on all errors returned by this package. Helper errors.WithDetails allows both recording a stack trace and annotating an error with details at the same time.
  • Errors and stack traces support configurable formatting and can be marshaled into JSON. Both formatting and JSON marshaling is supported also for errors not made using this package.
  • Limited JSON unmarshal of errors is supported to enable formatting of JSON errors.

Installation

This is a Go package. You can add it to your project using go get:

go get gitlab.com/tozd/go/errors

It requires Go 1.16 or newer.

Usage

See full package documentation with examples on pkg.go.dev.

Why a new Go errors package?

github.com/pkg/errors package is archived and not developed anymore, with many issues not addressed (primarily because many require some backward incompatible change). At the same time it has been made before Go 1.13 added official support for wrapping errors and it does not (and cannot, in backwards compatible way) fully embrace it. This package takes what is best from github.com/pkg/errors, but breaks things a bit to address many of the open issues community has identified since then and to modernize it to today's Go:

  • Message formatting WithMessage vs. Wrap: #114
  • Do not re-add stack trace if one is already there: #122
  • Be explicit when you want to record a stack trace again vs. do not if it already exists: #75 #158 #242
  • StackTrace() should return []uintptr: #79
  • Do not assume Cause cannot return nil: #89
  • Obtaining only message from Wrap: #93
  • WithMessage always prefixes the message: #102
  • Differentiate between "wrapping" and "causing": #112
  • Support for base errors: #130 #160
  • Support for a different delimiter by supporting Errorf: #207 #226
  • Support for Errorf wrapping an error: #244
  • Having each function wrap only once: #223

What are main differences from github.com/pkg/errors?

In most cases this package can be used as a drop-in replacement for github.com/pkg/errors, but there are some small (behavioral) differences (i.e., improvements):

  • The stackTracer interface's StackTrace() method returns []uintptr and not custom type StackTrace.
  • All error-wrapping functions return errors which implement the standard unwrapper interface, but only errors.Wrap records a cause error and returns an error which implements the causer interface.
  • All error-wrapping functions wrap the error into only one new error.
  • Only errors.Wrap always records the stack trace while other functions do not record if it is already present.
  • errors.Cause repeatedly unwraps the error until it finds one which implements the causer interface, and then return its cause.

Main additions are:

  • Errorf supports %w.
  • This package supports annotating errors with additional key-value details.
  • This package provides more configurable formatting and JSON marshaling of stack traces and errors.
  • Support for base errors (e.g., errors.Base and errors.WrapWith) instead of just operating on error messages.

How to migrate from github.com/pkg/errors?

  1. Replace your github.com/pkg/errors imports with gitlab.com/tozd/go/errors imports.
  2. Run go get gitlab.com/tozd/go/errors.

In most cases, this should be it. Consider:

  • Migrating to using errors.Errorf (which supports %w) as it subsumes most of github.com/pkg/errors's error constructors.
  • Using errors.E return type instead of plain error. This makes Go type system help you to not return an error without a stack trace.
  • Using structured details with errors.WithDetails.
  • Using base errors instead of message-based error constructors, especially if you want callers of your functions to handle different errors differently.

How to migrate from standard errors and fmt.Errorf?

  1. Replace your errors imports with gitlab.com/tozd/go/errors imports.
  2. Replace your fmt.Errorf calls with errors.Errorf calls.
  3. If you have top-level errors.New or fmt.Errorf/errors.Errorf calls to create variables with base errors, use errors.Base and errors.Basef instead. Otherwise your stack traces will not be recorded correctly.

This is it. Now all your errors automatically record stack traces. You can now print those stack traces when formatting errors using fmt.Printf("%+v", err) or marshal them to JSON.

Consider:

  • Using structured details with errors.WithDetails.
  • Using errors.E return type instead of plain error. This makes Go type system help you to not return an error without a stack trace.
  • Using base errors instead of message-based error constructors, especially if you want callers of your functions to handle different errors differently.

This package provides everything errors package does (but with stack traces and optional details) in a compatible way, often simply proxying calls to the standard errors package, but there is a difference how errors.Join operates: if only one non-nil error is provided, errors.Join returns it as-is without wrapping it. Furthermore, errors.Join records a stack trace at the point it was called.

How should this package be used?

Patterns for errors in Go have evolved through time, with Go 1.13 introducing error wrapping (and Go 1.20 wrapping of multiple errors) enabling standard annotation of errors with additional information. For example, standard fmt.Errorf allows annotating the error message of a base error with additional information, e.g.:

import (
  "errors"
  "fmt"
)

var ErrMy = errors.New("my error")

// later on

err := fmt.Errorf(`user "%s" made an error: %w`, username, ErrMy)

This is great because one can later on extract the cause of the error using errors.Is without having to parse the error message itself:

if errors.Is(err, ErrMy) {
  ...
}

But if ErrMy can happen at multiple places it is hard to debug without additional information where this error happened. One can add it to the error message, but this is tedious:

err := fmt.Errorf(`user "%s" made an error during sign-in: %w`, username, ErrMy)

// somewhere else

err := fmt.Errorf(`user "%s" made an error during sign-out: %w`, username, ErrMy)

Furthermore, if you need to extract the username you again have to parse the error message.

Instead, this package provides support for recording a stack trace optional structured details:

import (
  "gitlab.com/tozd/go/errors"
)

var ErrMy = errors.Base("my error")
var ErrUser = errors.Basef("user made an error: %w", ErrMy)

// later on

err := errors.WithDetails(ErrUser, "username", username)

Here, err contains a descriptive error message (without potentially sensitive information), a stack trace, and easy to extract username. You can display all that to the developer using fmt.Printf:

fmt.Printf("error: %#+v", err)

Extracting username is easy:

errors.AllDetails(err)["username"]

And if you need to know exactly which error happened (e.g., to show a translated error message to the end user), you can use errors.Is or similar logic (to map between errors and their user-friendly translated error messages).

The package provides errors.Errorf, errors.Wrap and other message-based error constructors, but they are provided primarily for compatibility (and to support patterns different from the suggested one below).

Suggestion

  • Use errors.Base, errors.Basef, errors.BaseWrap and errors.BaseWrapf to create a tree of constant base errors.
  • Do not use errors.Errorf but use errors.WithDetails with a base error to record both a stack trace and any additional structured details at point where the error occurs.
  • You can use errors.WithStack or errors.WithDetails to do the same with errors coming from outside of your codebase, as soon as possible. If it is useful to know at a glance where the error is coming from, consider using errors.WithMessage to prefix their error messages with the source of the error (e.g, external function name).
  • If you want to map one error to another while recording the cause, use errors.WrapWith. If you want to reuse the error message, use errors.Prefix or errors.Errorf (but only to control how messages are combined).
  • If errors coming from outside of your codebase do not provide adequate base errors and base errors are needed, use errors.WrapWith, errors.Prefix, or errors.Errorf (but only to control how messages are combined) to provide them yourself.
  • If during handling of an error another error occurs (e.g., in defer during cleanup) use errors.Join or errors.Errorf (but only to control how messages are joined) to join them all.
  • Use errors.E return type instead of plain error. This makes Go type system help you to not return an error without a stack trace.

Your functions should return only errors for which you provide base errors as well. Those base errors become part of your API. All additional (non-constant) information about the particular error goes into its stack trace and details.

Do not overdo the approach with base errors, e.g., do not make them too granular. Design them as you design your API and consider their use cases. Remember, errors also have stack traces to help you understand where are they coming from. This holds also for prefixing error messages, prefix them only if it makes error messages clearer and not to make it into something resembling a call trace.

Remember, error messages, stack traces, and details are for developers not end users. Be mindful if and how you expose them to end users.

I use base errors, but stack traces do not look right?

Let's assume you use errors.New to create a base error:

var ErrMyBase = errors.New("error")

Later on, you want to annotate it with a stack trace and return it from the function where the error occurred:

func run() error.E {
  // ... do something ...
  return errors.WithStack(ErrMyBase)
}

Sadly, this does not work as expected. If you use errors.New (or errors.Errorf) to create a base error, a stack trace is recorded at the point it was called. errors.WithStack then does nothing because it detects that a stack trace already exists. So the stack trace you get is from the point you created the base error.

You should create base errors using errors.Base, errors.Basef, errors.BaseWrap and errors.BaseWrapf. They create errors without stack traces. And errors.WithStack adds a stack trace at the point it was called.

It looks like Wrap should be named Cause or WithCause. Why it is not?

For legacy reasons because this package builds on shoulders of github.com/pkg/errors. Every modification to errors made through this package is done through wrapping so that original error is always available. Wrap wraps the error to records the cause. Cause exist as a helper to return the recorded cause.

Is this package a fork of github.com/pkg/errors?

No. This is a completely new package which has been written from scratch using current best practices and patterns in Go and Go errors. But one of its goals is to be (in most cases) a drop-in replacement for github.com/pkg/errors so it shares API with github.com/pkg/errors while providing at the same time new utility functions and new functionality.

It looks like most of the things provided by this package can be done using standard errors package?

That is the idea! One of the goals of this package is to learn from github.com/pkg/errors and update it to modern Go errors patterns. That means that it is interoperable with other errors packages and can be used in large codebases with mixed use of different errors packages. The idea is that you should be able to create errors which behave like errors from this package by implementing few interfaces and this package knows how to use other errors if they implement those interfaces, too.

What this package primarily provides are utility functions for common cases so that it is just easier to do "the right thing" and construct useful errors. It even calls into standard errors package itself for most of the heavy lifting. Despite many features this package provides this approach keeps it pretty lean.

This package uses github.com/pkg/errors under the hood!

No, it does not. It is a completely new package. But because it wants to be compatible with errors made by github.com/pkg/errors in large codebases with mixed use of different errors packages it has to depend on github.com/pkg/errors to get one type (errors.StackTrace) from it so that it can extract stack traces from those errors. It is using no code from github.com/pkg/errors.

BTW, this package itself does not require to import it to be able to extract all data from its errors. Interfaces used by this package do not use custom types. Another lesson learned from github.com/pkg/errors.

Related projects

  • github.com/cockroachdb/errors – Go errors with every possible feature you might ever need in your large project. Internally it uses deprecated github.com/pkg/errors. This package aims to stay lean and be more or less just a drop-in replacement for core Go errors and archived github.com/pkg/errors, but with stack traces and structured details (and few utility functions for common cases).
  • github.com/friendsofgo/errors – A fork of github.com/pkg/errors but beyond updating the code to error wrapping introduced in Go 1.13 it does not seem maintained.
  • github.com/go-errors/errors – Another small error package with stack trace recording support, but with different API than github.com/pkg/errors. It does not support structured details, extended formatting nor JSON marshal.
  • github.com/rotisserie/eris – Eris has some similar features, like recording a stack trace and support for JSON. It supports additional features, like more formatting options and built-in Sentry support. It can be used as a replacement for github.com/pkg/errors but some functions are missing (e.g., WithMessage, WithStack) while this package provides them. Eris also does not support structured details.
  • github.com/ztrue/tracerr – A simple library to create errors with a stack trace. It is able to also show snippets of source code when formatting stack traces. But it is slower than this package because it resolves stack traces at error creation time and not only when formatting. It also lacks many github.com/pkg/errors functions so it does not work as a drop-in replacement.
  • emperror.dev/errors – A drop-in replacement for github.com/pkg/errors with similar features to this package, embracing modern Go errors patterns (wrapping, sentinel/base errors, etc.) and sharing similar design goals. It supports structured details as well. It has different integrations with other packages and services. But under the hood there are small but important differences: this package supports %w in errors.Errorf, it does not use github.com/pkg/errors internally, you can obtain a stack trace from errors without importing this package, it supports more formatting options and JSON marshaling of errors.
  • github.com/axkit/errors – Supports recording a stack trace, structured details, severity levels, JSON marshaling, etc. But it inefficiently resolves stack traces at error creation time and not only when formatting. It requires importing the package to access the stack trace. Its API is different from github.com/pkg/errors.
  • github.com/efficientgo/core/errors – A minimal library for wrapping errors with stack traces. Does not provide errors.Errorf not does it support %w format verb. It is not possible to access unformatted stack trace.

GitHub mirror

There is also a read-only GitHub mirror available, if you need to fork the project there.

About

A Go package providing errors with a stack trace and structured details. Read-only mirror of https://gitlab.com/tozd/go/errors

Topics

Resources

License

Stars

Watchers

Forks

Sponsor this project

  •  
  •  

Packages

No packages published