Error Wrapping¶
Wrap and Wrapf add a layer of context to an existing error while preserving the cause chain. Each wrap captures its own stack frames, and inherited metadata stays attached so log records remain useful all the way out.
Signature¶
func Wrap(err error, msg string, opts ...Option) *Error
func Wrapf(err error, format string, args ...any) *Error
Both return nil if err is nil, so you can call them unconditionally:
Basic usage¶
The returned *Error:
- Has its own
Error()text:"input validation failed: <inner.Error()>". - Holds the inner error as its
Cause(), soerrors.Unwrap,errors.Is, anderrors.Aswalk through it. - Captures a fresh stack at the wrap site.
Stack semantics¶
root := db.Ping() // io error from net/http
inner := ewrap.Wrap(root, "ping db") // captures wrap site A
outer := ewrap.Wrap(inner, "boot") // captures wrap site B
inner.Stack() // shows where Wrap was called for `inner`
outer.Stack() // shows where Wrap was called for `outer`
outer.Stack() and inner.Stack() are independent. To see the full chain in one shot, use the verbose verb:
If you need the inner's frames too, walk the chain:
for cur := error(outer); cur != nil; cur = errors.Unwrap(cur) {
var ec *ewrap.Error
if errors.As(cur, &ec) {
fmt.Println(ec.Stack())
}
}
Wrapping inherits typed fields¶
When the inner error is a *Error, the wrapper inherits:
metadata(cloned viamaps.Cloneso wrapper writes don't mutate the inner)errorContext,recovery,retryobserver,loggerhttpStatus,retryable
You can override any of these by passing the corresponding option to Wrap.
Wrapping standard errors¶
These work like any other wrap; errors.Is(err, io.EOF) returns true and serializers walk the cause chain via errors.Unwrap.
Wrapf — formatted¶
If you need the wrapped error to participate in %w semantics, pass it through Newf instead, or wrap explicitly:
Adding context while wrapping¶
return ewrap.Wrap(err, "payment processing failed",
ewrap.WithContext(ctx, ewrap.ErrorTypeExternal, ewrap.SeverityCritical),
ewrap.WithHTTPStatus(http.StatusBadGateway),
ewrap.WithRetryable(true),
ewrap.WithLogger(logger),
).
WithMetadata("amount", amount).
WithMetadata("currency", "USD").
WithMetadata("processor", "stripe")
Conditional wrapping¶
err := db.Query(...)
switch {
case errors.Is(err, sql.ErrNoRows):
return ewrap.Wrap(err, "record not found",
ewrap.WithContext(ctx, ewrap.ErrorTypeNotFound, ewrap.SeverityWarning),
ewrap.WithHTTPStatus(http.StatusNotFound))
case errors.Is(err, sql.ErrConnDone):
return ewrap.Wrap(err, "database connection lost",
ewrap.WithContext(ctx, ewrap.ErrorTypeDatabase, ewrap.SeverityCritical),
ewrap.WithRetryable(true))
case err != nil:
return ewrap.Wrap(err, "database operation failed",
ewrap.WithContext(ctx, ewrap.ErrorTypeDatabase, ewrap.SeverityError))
}
Wrapping inside helpers — WrapSkip¶
If you wrap inside a helper, the captured stack starts in the helper rather than at the call site:
func wrapDB(err error, msg string) *ewrap.Error {
// BAD: stack starts here
return ewrap.Wrap(err, msg, ewrap.WithContext(...))
}
Use WrapSkip(skip, ...) to advance past the helper frames:
func wrapDB(err error, msg string) *ewrap.Error {
return ewrap.WrapSkip(1, err, msg, ewrap.WithContext(...))
}
The same pattern works for New via NewSkip.
Best practices¶
- One wrap per layer. Don't wrap the same error twice in the same function; that just doubles the message.
- Add information, not noise. A useful wrap message points at what this layer was doing, not just that it failed.
- Use
errors.Is/Asfor branching, not string matching on the rendered message. - Don't wrap simple validation errors in tight loops if you don't add context — return the inner error directly.
Performance¶
- A single
Wrapallocates the*Errorstruct plus the stack PCs slice (~2 allocations). - Inherited metadata is cloned shallowly via
maps.Clone. (*Error).Error()andStack()are cached on first call; subsequent reads on the wrapped error are lock-free.