Error Handling Libraries for Go and Whether You Need Any
Compare Go error handling libraries, pkg/errors, cockroachdb/errors, go-errors, errorx and go-multierror, with what the standard library already does.

Every few months someone asks which error handling library to use in Go, and the best answer is usually "probably none". That was not always true. Before Go 1.13 the standard library had no way to wrap an error, and pkg/errors filled the gap so well that its Wrap and Cause shaped the design of what came next. The standard library caught up, and what remains for a library to offer is narrower than it used to be.
Tested with Go 1.25.6, macOS on Apple Silicon (arm64); third-party libraries described from their documentation, not run here
This page compares the libraries you will meet, says what each adds beyond the standard library, and gives a way to decide. It belongs to the error handling guide. The descriptions of the third-party packages come from their documentation and READMEs; I did not run them for this page, so there is no output from them below. The standard library comparisons did run, on Go 1.25.6.
What the standard library covers
Start from what you already have, because it is more than most people remember.
| Need | Standard library |
|---|---|
| Create an error | errors.New, fmt.Errorf |
| Wrap with context | fmt.Errorf("...: %w", err) (Go 1.13) |
Several causes in one Errorf | multiple %w verbs (Go 1.20) |
| Ask "is this error in the chain" | errors.Is |
| Extract a typed error | errors.As |
| Combine many errors | errors.Join (Go 1.20) |
| Custom comparison | an Is(target) bool method |
| Walk the chain | errors.Unwrap, or Unwrap() []error |
All of that is a few functions and a convention, and the pages in this series cover each (wrapping, Is and As, Join). What the standard library leaves out is a short list: stack traces, a way to send errors between processes, and richer classification than sentinels and types.
Stack traces are thirty lines
The most common reason to reach for a library is the missing stack trace, and it is also the easiest to supply yourself. A wrapper that records runtime.Callers when the error is created, and prints the frames on request, is about thirty lines (stack traces for errors builds one and measures it: about 220 ns per capture against 89 ns for a plain wrap on an M1 Max). That makes a library worth it for stacks only if you want the convention and the formatting provided for you, or if you want every error in a large codebase to carry one.
The libraries
pkg/errors
github.com/pkg/errors is where much of this started. It added Wrap, WithMessage, Cause and stack-capturing constructors, and a %+v format that prints the trace. Its README now says that the package is moving into maintenance mode because of the Go 2 error proposals, and that it is not accepting proposals for new functionality, though it welcomes bug fixes. Its Cause function predates Unwrap, and modern versions interoperate with errors.Is and errors.As. You will meet it in older codebases. For new code, the standard library does the wrapping and you add a stack yourself if you need it.
cockroachdb/errors
github.com/cockroachdb/errors presents itself as a drop-in replacement for both pkg/errors and the standard errors package, with an emphasis on large distributed systems. Its README lists features the standard library lacks: stack trace capture in its standard wrappers, network portability (an error can be encoded to protobuf, sent across a service boundary and still answer errors.Is on the other side), support for details that are free of personal data, and opt-in reporting to Sentry that strips sensitive information. It is actively maintained. If you run a fleet of services that return errors to each other and want them to keep their identity across the wire, this is the one to evaluate. For a single binary it is more than you need.
go-errors/errors
github.com/go-errors/errors is a smaller package whose description is that it adds stack trace support to errors. It provides an *Error type that implements the standard error interface, so it can be used interchangeably with code expecting a normal error, and helpers to print the stack. It suits a project whose only requirement is "every error should show where it came from" and which wants a small dependency.
joomcode/errorx
github.com/joomcode/errorx aims higher on structure. Its README lists stack traces, composability of errors, enhancing an error with both a trace and a message, and type and trait checks. The idea is a namespace and type system for errors: you define error types in groups and attach traits (such as "temporary" or "timeout") that code can test. This overlaps with the design in domain errors and retryable errors, which you can build with a page of your own code; errorx gives you that machinery ready made, with a learning cost for every contributor.
hashicorp/go-multierror
github.com/hashicorp/go-multierror collected several errors into one, a job errors.Join now does. Its README opens with a note that as of Go 1.20 the standard library provides errors.Join, and that for new projects the maintainers recommend using it, with a migration guide from Append to the standard function. It still has extras, such as custom list formatting, so existing users have a reason to stay. New code has none.
golang.org/x/xerrors
golang.org/x/xerrors holds the transition packages for the Go 1.13 error values design. Its documentation says the functions were incorporated into the standard library's errors package in Go 1.13. It remains for code that must support older Go versions, and for its frame-printing helpers. You do not need it on a current toolchain.
The same jobs with the standard library
Two of the above reasons, joining and chain inspection, can be shown running with nothing imported:
package main
import (
"errors"
"fmt"
"io/fs"
)
func main() {
// What multierror.Append did.
var errs []error
errs = append(errs, errors.New("name is required"))
errs = append(errs, fmt.Errorf("config: %w", fs.ErrNotExist))
joined := errors.Join(errs...)
fmt.Println(joined)
// What Cause / Is / As did.
fmt.Println(errors.Is(joined, fs.ErrNotExist))
// Wrapping with context, the thing pkg/errors made popular.
wrapped := fmt.Errorf("start server: %w", fmt.Errorf("load config: %w", fs.ErrNotExist))
for e := error(wrapped); e != nil; e = errors.Unwrap(e) {
fmt.Printf("%T\n", e)
}
}name is required
config: file does not exist
true
*fmt.wrapError
*fmt.wrapError
*errors.errorStringThe chain walk shows the three layers, two wrappers and the sentinel, and errors.Is finds the cause through the join. That is the entire job Wrap, Cause and Append were created for.
How to decide
Ask four questions, in order.
- Do you need stack traces on errors? If only on panics and at a few boundaries, no: use
debug.Stack()atrecover. If on every error across a large codebase, a library or a thirty-line wrapper. - Do errors cross process boundaries and need to keep their identity? If yes, evaluate cockroachdb/errors or encode your own error codes into the wire format. If no, skip the question.
- Do you want a prebuilt classification scheme? If your team will actually define and use traits and namespaces, errorx; otherwise a
Kindtype of your own is a page of code. - Is the codebase already using one? Then keep using it consistently, and do not mix: two stack-carrying error types in one program print differently and confuse each other's helpers.
If the answers are no, no, no, you want the standard library, and you are in good company: much of the Go ecosystem runs on it. A fifth consideration is the cost of a dependency. An error library is imported by nearly every package in your project, so its API leaks into your function bodies, its types appear in your logs, and a breaking change or abandonment affects everything at once. That is a reason to wrap it behind a tiny package of your own (internal/errs), so swapping later is one directory.
Tools beyond libraries
Several helpers are not error libraries at all and are worth having. errcheck finds dropped errors (errcheck). go vet catches format mistakes in Errorf. Linters such as errorlint and nilerr find the comparison and nil-return mistakes from the code review list. golang.org/x/sync/errgroup is the standard answer for the first error from a group of goroutines (errgroup). And Sentry-style reporters take errors from any of the above and group them for humans.
The packages' own pages are the source of truth for what they offer today: pkg/errors, cockroachdb/errors, go-errors/errors, errorx and go-multierror. Read their READMEs before you commit, and read the Go blog on error values to see what the standard library already gives you.


