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)
- ✅ Masked envelope — expose a safe public-facing error while keeping the cause in the 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 the Domain-Specific Errors (Killer Feature) section for more details.
- Installation
- Error Codes & Metadata
- Wrapping & Cause Chains
- JSON & Structured Logging
- Field Errors for Validation
- 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.
joined := errors.Join(
xrr.New("first", "EC_FIRST"),
xrr.New("second", "EC_SECOND"),
)
for _, p := range xrr.Split(joined) {
fmt.Println(p)
}
// Output:
// first
// secondcombined := xrr.Join(
xrr.New("first", "EC_FIRST"),
nil,
xrr.New("second", "EC_SECOND"),
)
fmt.Println(xrr.IsJoined(combined))
for _, p := range xrr.Split(combined) {
fmt.Println(p)
}
// Output:
// true
// first
// secondsingle := xrr.New("single error", "EC_SINGLE")
joined := errors.Join(
xrr.New("first", "EC_FIRST"),
xrr.New("second", "EC_SECOND"),
)
fmt.Println(xrr.IsJoined(single))
fmt.Println(xrr.IsJoined(joined))
// Output:
// false
// trueDefaultCode is useful when you have multiple possible codes and want a fallback.
code := xrr.DefaultCode("ECFallback", "", "EC_FOUND", "EC_IGNORED")
fmt.Println(code)
// Output:
// EC_FOUNDAll 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.
cause := xrr.New("cause", "EC_CAUSE")
lead := xrr.New("lead", "EC_LEAD")
err := xrr.Envelop(cause, lead)
fmt.Printf("is cause: %v\n", errors.Is(err, cause))
fmt.Printf("is lead: %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 cause: true
// is lead: true
// unwrap: cause
// message: cause
// {
// "code": "EC_LEAD",
// "error": "lead",
// "errors": [
// {
// "code": "EC_CAUSE",
// "error": "cause"
// }
// ]
// }When the cause is a joined error:
cause := errors.Join(xrr.New("cause A", "EC_A"), xrr.New("cause B", "EC_B"))
lead := xrr.New("lead", "EC_LEAD")
err := xrr.Envelop(cause, lead)
fmt.Printf("%s\n", must.Value(json.MarshalIndent(err, "", " ")))
// Output:
// {
// "code": "EC_LEAD",
// "error": "lead",
// "errors": [
// {
// "code": "EC_A",
// "error": "cause A"
// },
// {
// "code": "EC_B",
// "error": "cause B"
// }
// ]
// }When the cause contains field errors:
fields := map[string]error{
"a": xrr.New("cause A", "EC_A"),
"b": xrr.New("cause B", "EC_B"),
}
cause := xrr.NewFieldErrors(fields)
lead := xrr.New("lead", "EC_LEAD")
err := xrr.Envelop(cause, lead)
fmt.Printf("%s\n", must.Value(json.MarshalIndent(err, "", " ")))
// Output:
// {
// "code": "EC_LEAD",
// "error": "lead",
// "fields": {
// "a": {
// "code": "EC_A",
// "error": "cause A"
// },
// "b": {
// "code": "EC_B",
// "error": "cause B"
// }
// }
// }Masked exposes the lead to callers while keeping cause accessible
in the Go error chain. Use it to present a clean public-facing error without
leaking internal details.
cause := xrr.New("db: connection refused", "EC_DB_ERR")
lead := xrr.New("service unavailable", "EC_SERVICE_UNAVAILABLE")
err := xrr.Mask(cause, lead)
fmt.Printf("is cause: %v\n", errors.Is(err, cause))
fmt.Printf("is lead: %v\n", errors.Is(err, lead))
fmt.Printf("message: %v\n", err.Error())
fmt.Printf("%s\n", must.Value(json.MarshalIndent(err, "", " ")))
// Output:
// is cause: true
// is lead: true
// message: service unavailable
// {
// "code": "EC_SERVICE_UNAVAILABLE",
// "error": "service unavailable"
// }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 are welcome. Please open an issue or pull request.
MIT License — see LICENSE.md file.