Automatic github.com/pkg/errors → github.com/go-extras/errx migrator.
pkg/errors is no longer actively maintained — it has been in maintenance mode for years (its last
release, v0.9.1, is from January 2020) and now accepts only bug fixes, not new functionality.
errx-migrate is a small, type-aware AST rewriter that converts a pkg/errors-based codebase to
errx safely and idempotently — leaving
gofmt-clean output and explicit // TODO(errx-migrate) markers wherever a conversion cannot be proven safe.
It is type-aware: it uses golang.org/x/tools/go/packages
to resolve which errors identifier is actually pkg/errors (handling aliases, dot-imports, and shadowing),
so it never rewrites stdlib errors calls by mistake.
go install github.com/go-extras/errx-migrate/cmd/errx-migrate@latest# Preview a migration as a unified diff (no files written):
errx-migrate -diff ./...
# Apply the migration in place:
errx-migrate -w ./...
# Lean mode: map to the zero-dependency errx core + stdlib instead of the stacktrace subpackage:
errx-migrate -w -no-stack ./...
# Compat mode: route through the errx compat package (errxc), which accepts plain error classifications:
errx-migrate -w -compat ./...Flags:
| Flag | Description |
|---|---|
-w |
Write changes back to source files (default: print the result to stdout). |
-diff |
Print a unified diff instead of writing or printing the full file. |
-no-stack |
Lean mode — map to the dependency-free errx core + stdlib errors/fmt, dropping implicit stack capture. The default is stack-preserving mode. |
-compat |
Compat mode — route the message/wrap family through the errx/compat package (imported as errxc), whose functions accept plain error classifications. Like lean it drops implicit stacks, but an explicit errors.WithStack is still preserved as errxtrace.Classify. Mutually exclusive with -no-stack. |
-include-generated |
Also rewrite generated files (// Code generated ... DO NOT EDIT.). Skipped by default. |
When they are imported, errx/stacktrace is aliased as errxtrace and errx/compat as errxc. (errx/stacktrace may not appear at all in a lean or compat run if no errors.WithStack rewrite needs it.)
The tool must be able to type-check the target packages, so run it from the module root with the relevant
build tags/GOOS set if you have platform-specific files.
Stack-preserving (default). Stack-capturing pkg/errors calls are mapped to the errx/stacktrace
subpackage (imported as errxtrace) so runtime behavior is unchanged.
Lean (-no-stack). Implicit stack capture is dropped and calls map to the zero-dependency errx core plus
stdlib errors/fmt. Explicit errors.WithStack is still preserved as errxtrace.Classify (an explicit
request for a stack is honored).
Compat (-compat). The message/wrap family is routed through the errx/compat package (imported as
errxc), whose functions accept plain error classifications rather than errx.Classified — handy when you
also classify with third-party error values. Like lean it carries no implicit stack; an explicit
errors.WithStack is still preserved as errxtrace.Classify. The migrator always prefers errx/errxtrace
where possible and only routes through compat in this mode.
pkg/errors |
stack-preserving (default) | lean (-no-stack) |
compat (-compat) |
|---|---|---|---|
errors.New(msg) |
errxtrace.ClassifyNew(msg) |
errors.New(msg) (stdlib) |
errxc.ClassifyNew(msg) |
errors.Errorf(f, a…) |
errxtrace.ClassifyNew(fmt.Sprintf(f, a…)) |
fmt.Errorf(f, a…) |
errxc.ClassifyNew(fmt.Sprintf(f, a…)) |
errors.Wrap(err, msg) |
errxtrace.Wrap(msg, err) |
errx.Wrap(msg, err) |
errxc.Wrap(msg, err) |
errors.Wrapf(err, f, a…) |
errxtrace.Wrap(fmt.Sprintf(f, a…), err) |
errx.Wrap(fmt.Sprintf(f, a…), err) |
errxc.Wrap(fmt.Sprintf(f, a…), err) |
errors.WithMessage(err, msg) |
errx.Wrap(msg, err) |
errx.Wrap(msg, err) |
errxc.Wrap(msg, err) |
errors.WithMessagef(err, f, a…) |
errx.Wrap(fmt.Sprintf(f, a…), err) |
same | errxc.Wrap(fmt.Sprintf(f, a…), err) |
errors.WithStack(err) |
errxtrace.Classify(err) |
errxtrace.Classify(err) |
errxtrace.Classify(err) |
errors.Is / As / Unwrap |
stdlib errors.* |
same | same |
errors.Cause(err) |
inline root-unwrap + // TODO(errx-migrate) |
same | same |
Notes:
- Argument order swaps.
pkg/errors.Wrap(err, msg)becomeserrx.Wrap(msg, err)— the cause and message arguments are reordered. - Nil semantics match.
pkg/errors.Wrap(nil, …)anderrx.Wrap(…, nil)both returnnil. fmt.Sprintfinjection is flagged. Where a dynamic message is injected (Wrapf,WithMessagef, and stack-modeErrorf), a// TODO(errx-migrate)notes that a dynamic error message is non-idiomatic and a constant message string is preferred.errors.Causeis rewritten to an inline root-unwrap loop with a// TODO(errx-migrate)recommending you review it (oftenerrors.Is/Asis the better fit).%+vstack formatting is left untouched; with stack-preserving mode and a recenterrx, the captured trace still renders via%+v.
The migrator is deliberately conservative: when it cannot prove a rewrite safe, it leaves the call in place
with a // TODO(errx-migrate) comment for manual review rather than guess. In practice this happens for:
- a
pkg/errorscall buried inside another (for example inside a closure passed to the call being wrapped); - a
Wrapf/Errorf/WithMessagefwhose format string contains%w(whichfmt.Sprintfcannot render); - a comment sitting between the arguments of a call whose arguments are reordered.
Other notes:
- Build tags /
GOOS. Only files that type-check for the currentGOOS/GOARCHare rewritten. Re-run withGOOS=windows,GOOS=darwin, etc. to cover platform-specific files. - Test files are migrated too.
- The target packages must type-check (the tool relies on full type information).
The rewriter is also exposed as a go/analysis Analyzer
that emits SuggestedFixes, so it can run under go vet, gopls, or golangci-lint.
make build # build bin/errx-migrate
make test # go test -race ./...
make lint # golangci-lint run ./...
make fmt # golangci-lint run --fix ./...MIT — see LICENSE.