Advanced Examples¶
Realistic scenarios combining multiple ewrap features. Each example is self-contained and uses the current API.
HTTP service with classification + retry¶
package billing
import (
"context"
"net/http"
"time"
"github.com/hyp3rd/ewrap"
"github.com/hyp3rd/ewrap/breaker"
)
type Service struct {
upstream Upstream
breaker *breaker.Breaker
logger ewrap.Logger
}
func (s *Service) Charge(ctx context.Context, req ChargeRequest) (*Receipt, error) {
if err := req.Validate(); err != nil {
return nil, ewrap.Wrap(err, "invalid charge request",
ewrap.WithContext(ctx, ewrap.ErrorTypeValidation, ewrap.SeverityWarning),
ewrap.WithHTTPStatus(http.StatusUnprocessableEntity),
ewrap.WithSafeMessage("invalid charge request"),
ewrap.WithLogger(s.logger))
}
if !s.breaker.CanExecute() {
return nil, ewrap.New("payments breaker open",
ewrap.WithContext(ctx, ewrap.ErrorTypeExternal, ewrap.SeverityWarning),
ewrap.WithHTTPStatus(http.StatusServiceUnavailable),
ewrap.WithRetryable(true),
ewrap.WithRetry(3, 2*time.Second),
ewrap.WithLogger(s.logger))
}
receipt, err := s.upstream.Charge(ctx, req)
if err != nil {
s.breaker.RecordFailure()
return nil, ewrap.Wrap(err, "upstream charge",
ewrap.WithContext(ctx, ewrap.ErrorTypeExternal, ewrap.SeverityError),
ewrap.WithHTTPStatus(http.StatusBadGateway),
ewrap.WithRetryable(true),
ewrap.WithSafeMessage("payment provider error"),
ewrap.WithRecoverySuggestion(&ewrap.RecoverySuggestion{
Message: "Retry after backoff; check provider status page.",
Documentation: "https://runbooks.example.com/payments/upstream",
}),
ewrap.WithLogger(s.logger)).
WithMetadata("provider", "stripe").
WithMetadata("amount_cents", req.AmountCents)
}
s.breaker.RecordSuccess()
return receipt, nil
}
The handler then translates uniformly:
func (h *Handler) Charge(w http.ResponseWriter, r *http.Request) {
receipt, err := h.svc.Charge(r.Context(), parseRequest(r))
if err != nil {
status := ewrap.HTTPStatus(err)
if status == 0 {
status = http.StatusInternalServerError
}
msg := err.Error()
if e, ok := err.(*ewrap.Error); ok {
msg = e.SafeError()
}
http.Error(w, msg, status)
return
}
writeJSON(w, receipt)
}
Retry middleware¶
func WithRetry(max int, base time.Duration, op func(context.Context) error) func(context.Context) error {
return func(ctx context.Context) error {
delay := base
for attempt := 1; attempt <= max; attempt++ {
err := op(ctx)
if err == nil {
return nil
}
if !ewrap.IsRetryable(err) {
return err
}
select {
case <-time.After(delay):
delay *= 2
case <-ctx.Done():
return ewrap.Wrap(ctx.Err(), "retry budget exhausted",
ewrap.WithRetryable(false))
}
}
// last attempt — return whatever the op returns
return op(ctx)
}
}
IsRetryable walks the chain and consults Temporary() as a fallback, so this middleware works with any error source — ewrap or stdlib.
Validation middleware emitting structured 422¶
func writeValidation(w http.ResponseWriter, err error) {
eg, ok := err.(*ewrap.ErrorGroup)
if !ok {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
body, _ := eg.ToJSON()
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusUnprocessableEntity)
_, _ = w.Write([]byte(body))
}
The ErrorGroup JSON envelope already carries every member's field metadata, so the API consumer gets a clean per-field error list with no extra translation.
Background worker with the breaker¶
type Worker struct {
queue <-chan Job
cb *breaker.Breaker
logger ewrap.Logger
obs ewrap.Observer
}
func (w *Worker) Run(ctx context.Context) {
for {
select {
case <-ctx.Done():
return
case job := <-w.queue:
if !w.cb.CanExecute() {
w.requeue(job, time.Second)
continue
}
if err := w.process(ctx, job); err != nil {
w.cb.RecordFailure()
wrapped := ewrap.Wrap(err, "processing job",
ewrap.WithContext(ctx, ewrap.ErrorTypeInternal, ewrap.SeverityError),
ewrap.WithLogger(w.logger),
ewrap.WithObserver(w.obs)).
WithMetadata("job_id", job.ID)
wrapped.Log()
if ewrap.IsRetryable(wrapped) {
w.requeue(job, backoff(job.Attempts))
}
continue
}
w.cb.RecordSuccess()
}
}
}
A single WithLogger/WithObserver near the construction site propagates to wraps via inheritance.
Fan-out fan-in with ErrorGroup¶
var pool = ewrap.NewErrorGroupPool(16)
func sweep(ctx context.Context, ids []string) error {
eg := pool.Get()
defer eg.Release()
var wg sync.WaitGroup
for _, id := range ids {
wg.Add(1)
go func(id string) {
defer wg.Done()
if err := refresh(ctx, id); err != nil {
eg.Add(ewrap.Wrap(err, "refresh failed",
ewrap.WithContext(ctx, ewrap.ErrorTypeNetwork, ewrap.SeverityWarning),
ewrap.WithRetryable(true)).
WithMetadata("id", id))
}
}(id)
}
wg.Wait()
return eg.Join() // single error containing every failure
}
Each member error preserves its own stack trace, metadata, and HTTP status; the aggregator returned by Join() is errors.Is-walkable.
Custom domain factory¶
package billing
import (
"context"
"net/http"
"time"
"github.com/hyp3rd/ewrap"
)
type Code int
const (
CodeUnknown Code = iota
CodeCardDeclined
CodeRateLimited
)
// New constructs a billing-specific error. NewSkip(1, ...) advances the
// captured stack past this helper so the trace begins at the caller.
func New(ctx context.Context, code Code, msg string) *ewrap.Error {
base := ewrap.NewSkip(1, msg,
ewrap.WithContext(ctx, ewrap.ErrorTypeExternal, ewrap.SeverityWarning))
switch code {
case CodeCardDeclined:
return base.
WithMetadata("billing_code", code).
WithContext(&ewrap.ErrorContext{
Type: ewrap.ErrorTypeExternal,
Severity: ewrap.SeverityWarning,
})
case CodeRateLimited:
return base.
WithMetadata("billing_code", code).
WithMetadata("retry_after_s", 30)
}
return base.WithMetadata("billing_code", code)
}
Usage:
OpenTelemetry observer¶
import "go.opentelemetry.io/otel/trace"
type otelObserver struct {
tracer trace.Tracer
ctx context.Context // captured on construction
}
func (o *otelObserver) RecordError(message string) {
span := trace.SpanFromContext(o.ctx)
if !span.IsRecording() {
return
}
span.RecordError(errors.New(message))
}
err := ewrap.New("payment failed",
ewrap.WithObserver(&otelObserver{tracer: tracer, ctx: ctx}))
err.Log() // span event recorded
For a richer integration, walk the metadata in the observer and attach each as a span attribute.
Test fixtures¶
package billing
import (
"errors"
"testing"
"github.com/hyp3rd/ewrap"
)
var errFakeUpstream = errors.New("fake upstream failure")
func TestChargeWrapsUpstreamError(t *testing.T) {
t.Parallel()
svc := &Service{upstream: stubUpstream{err: errFakeUpstream}}
_, err := svc.Charge(t.Context(), validRequest())
if !errors.Is(err, errFakeUpstream) {
t.Fatalf("expected upstream error in chain, got %v", err)
}
if got := ewrap.HTTPStatus(err); got != http.StatusBadGateway {
t.Errorf("HTTP status: got %d, want %d", got, http.StatusBadGateway)
}
if !ewrap.IsRetryable(err) {
t.Error("expected upstream error to be retryable")
}
}
errors.Is, ewrap.HTTPStatus, and ewrap.IsRetryable are the right assertions here — none of them touch string-formatted messages.