#### ##### # # ###### ##### ##### #### ##### ####
# # # # # # # # # # # # # # #
# # ## ##### # # # # # # # # ####
# # ## # ##### ##### # # ##### #
# # # # # # # # # # # # # # # #
#### # # # ###### # # # # #### # # ####
fuck yeah, another Go error handling package 🖕
A Go library that wraps errors with context information (file, line, function) because debugging without context is like trying to find your dick in the dark.
- Installation
- What the fuck does it do?
- Usage
- Error output
- Error mapping
- Common sentinels
- Documentation
- Sponsor
- License
- Why?
go get github.com/psyb0t/ctxerrorsNeeds Go 1.26 or newer. The library itself imports only the standard library.
This package automatically captures where your errors happen in your code. No more hunting through logs like some dickless detective wondering where the fuck that error came from.
- New() creates a new error with location context.
- Wrap() wraps an existing error with additional context and location.
- Wrapf() is Wrap() with printf-style formatting, because we're not animals.
- Join() squashes a pile of errors into one that still knows where they got squashed. For when one thing fans out and several bits can shit the bed independently: three log sinks, a batch of rows, whatever.
- SetErrorMap() / MapError() / ClearErrorMap() translate foreign sentinel errors (gorm, sql, redis...) into your own business errors at wrap time.
New(), Wrap(), Wrapf() and Join() return a plain error holding a *CTXError, which supports errors.Unwrap(), errors.Is() and errors.As() because Go's error handling conventions aren't completely ass-backwards. Wrap(nil, ...) and Wrapf(nil, ...) return nil and log the call site through log/slog, and Join() returns nil when everything you gave it is nil. The error map functions don't return anything; they just manage the translation map.
import "github.com/psyb0t/ctxerrors"
func processFile(filename string) error {
file, err := os.Open(filename)
if err != nil {
return ctxerrors.Wrap(err, "failed to open file")
}
defer file.Close()
// do stuff...
return nil
}
func writeToAllSinks(sinks []Sink, record Record) error {
var errs []error
for i, sink := range sinks {
if err := sink.Write(record); err != nil {
errs = append(errs, ctxerrors.Wrapf(err, "sink %d failed", i))
}
}
return ctxerrors.Join(errs...)
}Wrapping errors covers New, Wrap, Wrapf and Join, the nil handling and how errors.Is/errors.As see the result.
When shit hits the fan, you get detailed context:
failed to connect to database at localhost:5432: dial tcp [::1]:5432: connect: connection refused [/path/to/your/file.go:42 in main.connectToDatabase]
Wrap it again and every layer adds its own location, so one line shows the whole path the error took. Error output has the exact format, longer chains, and what Go does to your closure names in the trace.
Stop foreign errors from leaking through your layers. Register a translation map at startup and Wrap/Wrapf tag matching driver errors with your own business errors while wrapping:
ctxerrors.SetErrorMap(map[error]error{
gorm.ErrRecordNotFound: commerr.ErrNotFound,
gorm.ErrDuplicatedKey: commerr.ErrAlreadyExists,
})The wrapped error matches both commerr.ErrNotFound and the original gorm error with errors.Is, and Error() prints the mapped sentinel so driver details stay out of logs. CTXError.Original() hands you the raw cause when you really need it. The matching rules are on Error mapping.
commerr is the shared sentinel vocabulary (not-found, already-exists, timeout, the usual suspects) that every service ends up needing, parked in a subpackage so importing ctxerrors just to wrap shit doesn't drag the whole list in:
import "github.com/psyb0t/ctxerrors/commerr"
func GetUser(id int) (*User, error) {
if id <= 0 {
return nil, commerr.ErrInvalidArgument
}
// ...
return nil, commerr.ErrNotFound
}They're plain errors.New sentinels, so errors.Is(err, commerr.ErrNotFound) keeps holding no matter how many times you Wrap it. All 60 of them, with their messages, are on Common sentinels.
The full docs live at psyb0t.github.io/ctxerrors.
| Page | What's in it |
|---|---|
| Wrapping errors | New, Wrap, Wrapf, Join, nil handling, unwrapping |
| Error output | The exact Error() format and a pile of stupid examples |
| Error mapping | Translating driver errors, matching rules, Original() and Mapped() |
| Common sentinels | Every commerr sentinel and its message |
| API reference | Every exported function and method |
| Development | Make targets, coverage gate, CI and releases |
MIT License, because lawyers are expensive and I don't want to deal with that shit
Because Go's error handling is verbose as fuck and debugging without context is like trying to find your asshole with both hands tied behind your back. This shit makes it slightly less painful.
