Supercharged errors for Go — stable codes, typed metadata, domain-specific types, and full errors/json/slog compatibility.
Plain Go errors are just strings. In real production applications you quickly run into limitations:
- No stable error codes for monitoring, alerting, or client-side handling
- No easy way to attach structured metadata for logs and API responses
- No clean isolation between error domains (Payment vs Auth vs User)
- Manual work for JSON serialization, slog integration, and rich formatting
xrr solves all of this while staying 100% compatible with Go’s standard library error handling, errors.Is, errors.As, errors.Join, %w wrapping, and ecosystem tools.
- ✅ Stable error codes — never change even if messages are updated
- ✅ Typed metadata — attach numbers, strings, times, durations with a fluent builder
- ✅ Domain-specific errors — generics-powered types per subsystem (
PaymentError,AuthError, ...) - ✅ Full stdlib compatibility — works seamlessly with
errors,fmt,json,slog - ✅ Production-ready outputs — automatic JSON marshaling, slog maps, custom
%+vformatting - ✅ Validation support — first-class field errors with clean JSON serialization
- ✅ Envelope pattern — ideal for API responses (lead error + full cause chain)
- ✅ Rich testing helpers —
xrrtestpackage with domain-aware assertions - ✅ Zero surprises — no reflection, minimal allocations, Go 1.26+
go get github.com/ctx42/xrrerr := xrr.New("user not found", "EC_USER_NOT_FOUND",
xrr.Meta().Int("attempt", 3).Str("user_id", "u-123").Option(),
)
fmt.Printf("%v\n", err)
fmt.Printf("%+v\n", err)
fmt.Println(xrr.GetCode(err))
fmt.Println(xrr.GetMeta(err))
// Output:
// user not found
// user not found (EC_USER_NOT_FOUND)
// EC_USER_NOT_FOUND
// map[attempt:3 user_id:u-123]Define isolated, type-safe errors for each subsystem. No more stringly-typed error checks.
// payment/errors.go
package payment
type edPayment struct{} // unexported marker — never exported
type PaymentError = xrr.GenericError[edPayment]
var (
NewPaymentError = xrr.ErrorFunc[edPayment]()
NewPaymentErrorf = xrr.ErrorfFunc[edPayment]()
)
// Usage
err := NewPaymentError("insufficient funds",
xrr.WithCode("EC_INSUFFICIENT_FUNDS"),
xrr.Meta().Float64("amount", 99.50).Option(),
)Now you get full compile-time safety:
type edPayment struct{}
type PaymentError = xrr.GenericError[edPayment]
newPaymentError := xrr.ErrorFunc[edPayment]()
err := newPaymentError("insufficient funds", "EC_INSUFFICIENT_FUNDS")
var pe *PaymentError
fmt.Println(errors.As(err, &pe))
fmt.Println(xrr.IsDomain[edPayment](err))
// Output:
// true
// trueSee full documentation in the Domain-Specific Errors section.
- Installation
- Error Codes & Metadata
- Wrapping & Cause Chains
- JSON & Structured Logging
- Field Errors for Validation
- Domain-Specific Errors
- API Envelope Pattern
- Error Inspection Utilities
- Testing with xrrtest
- Comparison with Alternatives
- Contributing
- License
go get github.com/ctx42/xrr@latestMinimum Go version: 1.26+
err := xrr.New("payment failed", "EC_PAYMENT_FAILED",
xrr.Meta().
Str("order_id", "ord-456").
Int64("user_id", 12345).
Float64("amount", 199.99).
Bool("retryable", true).
Option(),
)
fmt.Println(xrr.GetCode(err))
orderID, _ := xrr.GetStr(err, "order_id")
amount, _ := xrr.GetFloat64(err, "amount")
retryable, _ := xrr.GetBool(err, "retryable")
fmt.Println(orderID, amount, retryable)
// Output:
// EC_PAYMENT_FAILED
// ord-456 199.99 trueerr := fmt.Errorf("connection refused")
wrapped := xrr.Wrap(err, xrr.WithCode("EC_CONN"))
fmt.Println(errors.Is(wrapped, err))
fmt.Println(xrr.GetCode(wrapped))
fmt.Println(wrapped.Error())
// Output:
// true
// EC_CONN
// connection refusederr := fmt.Errorf("connection refused")
wrapped := xrr.New("dial failed", "EC_CONN", xrr.WithCause(err))
fmt.Println(errors.Is(wrapped, err))
fmt.Println(xrr.GetCode(wrapped))
fmt.Println(wrapped.Error())
// Output:
// true
// EC_CONN
// dial failed: connection refusedFull support for errors.Is, errors.As, and %w.
All errors implement json.Marshaler and provide GetMeta() for slog.
meta := xrr.Meta().Int("attempt", 3).Str("user_id", "u-123")
err := xrr.New("user not found", "EC_USER_NOT_FOUND", meta.Option())
fmt.Printf("%s\n", must.Value(json.MarshalIndent(err, "", " ")))
// Output:
// {
// "code": "EC_USER_NOT_FOUND",
// "error": "user not found",
// "meta": {
// "attempt": 3,
// "user_id": "u-123"
// }
// }meta := xrr.Meta().Int("attempt", 3).Str("user_id", "u-123")
err := xrr.New("user not found", "EC_USER_NOT_FOUND", meta.Option())
handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
ReplaceAttr: func(_ []string, a slog.Attr) slog.Attr {
if a.Key == slog.TimeKey {
return slog.Attr{}
}
return a
},
})
slog.New(handler).Error(
err.Error(),
"code", xrr.GetCode(err),
"meta", xrr.GetMeta(err),
)
// Output:
// {"level":"ERROR","msg":"user not found","code":"EC_USER_NOT_FOUND","meta":{"attempt":3,"user_id":"u-123"}}ts := time.Date(2026, 5, 9, 11, 23, 0, 0, time.UTC)
err := xrr.New("user not found", "EC_USER_NOT_FOUND",
xrr.Meta().Int("attempt", 3).Str("user_id", "u-123").Time("timestamp", ts).Option(),
)
handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
ReplaceAttr: func(_ []string, a slog.Attr) slog.Attr {
if a.Key == slog.TimeKey {
return slog.Attr{}
}
return a
},
})
slog.New(handler).Error(
"request failed",
"code", xrr.GetCode(err),
"meta", xrr.GetMeta(err),
)
fmt.Printf("%s\n", must.Value(json.MarshalIndent(err, "", " ")))
// Output:
// {"level":"ERROR","msg":"request failed","code":"EC_USER_NOT_FOUND","meta":{"attempt":3,"timestamp":"2026-05-09T11:23:00Z","user_id":"u-123"}}
// {
// "code": "EC_USER_NOT_FOUND",
// "error": "user not found",
// "meta": {
// "attempt": 3,
// "timestamp": "2026-05-09T11:23:00Z",
// "user_id": "u-123"
// }
// }Perfect for REST APIs and observability platforms.
fields := map[string]error{
"username": errors.New("username not found"),
"email": xrr.New(
"invalid email",
"EC_INVALID_EMAIL",
xrr.Meta().Str("action", "context").Option(),
),
}
err := xrr.NewFieldErrors(fields)
fmt.Printf("%s\n", must.Value(json.MarshalIndent(err, "", " ")))
// Output:
// {
// "email": {
// "code": "EC_INVALID_EMAIL",
// "error": "invalid email",
// "meta": {
// "action": "context"
// }
// },
// "username": {
// "code": "ECGeneric",
// "error": "username not found"
// }
// }Serializes cleanly to JSON arrays/objects for frontend consumption.
type edPayment struct{}
type PaymentError = xrr.GenericError[edPayment]
newPaymentError := xrr.ErrorFunc[edPayment]()
err := newPaymentError("insufficient funds", "EC_INSUFFICIENT_FUNDS")
var pe *PaymentError
fmt.Println(errors.As(err, &pe))
fmt.Println(xrr.IsDomain[edPayment](err))
// Output:
// true
// truecause := xrr.New("cause", "EC_CAUSE")
lead := xrr.New("lead", "EC_LEAD")
err := xrr.Enclose(cause, lead)
fmt.Printf("is lead error: %v\n", errors.Is(err, cause))
fmt.Printf("id db error: %v\n", errors.Is(err, lead))
fmt.Printf("unwrap: %v\n", errors.Unwrap(err))
fmt.Printf("message: %v\n", err.Error())
fmt.Printf("%s\n", must.Value(json.MarshalIndent(err, "", " ")))
// Output:
// is lead error: true
// id db error: true
// unwrap: cause
// message: cause
// {
// "code": "EC_LEAD",
// "error": "lead",
// "errors": [
// {
// "code": "EC_CAUSE",
// "error": "cause"
// }
// ]
// }import "github.com/ctx42/xrr/pkg/xrr/xrrtest"
xrrtest.AssertError[payment.EdPayment](t, err)
xrrtest.AssertCode(t, "EC_...", err)
xrrtest.AssertStr(t, "key", "expected", err)| Feature | std error |
pkg/errors | go-errors | xrr |
|---|---|---|---|---|
| Stable error codes | No | No | No | Yes |
| Typed metadata | No | No | Limited | Yes (fluent) |
| Domain generics | No | No | No | Yes |
| Built-in JSON | No | No | No | Yes |
| slog integration | Manual | Manual | Manual | Native |
| Field validation errors | No | No | No | Yes |
| Full errors.Is/As | Yes | Yes | Yes | Yes |
Contributions welcome! See CONTRIBUTING.md (if present) or open issues/PRs.
MIT License — see LICENSE file.
go get github.com/ctx42/xrr
Full godoc: pkg.go.dev/github.com/ctx42/xrr