Bridge Patterns
The architecture rule is libraries classify, applications enrich. A library knows its domain contract (404 = Rejection, timeout = Transient) but must not presume the application’s observability stack — so it imports go-error-family only and never imports oops. The application layer imports oops for enrichment (stack traces, trace IDs, request context) and reaches for the bridge/ submodule only where it creates its own errors and needs them classified.
LIBRARY LAYER imports go-error-family ONLY, never oops │ returns classified errors (NewRejection, NewTransient, ...) ▼APPLICATION LAYER imports oops + bridge; enriches with stack traces, │ trace IDs, domain context ▼BOUNDARY errorfamily.HTTPHandler / HandleError classifies once → HTTP status / exit codeThe three patterns below come from the bridge reference implementation — a runnable HTTP server demonstrating each one end-to-end.
Pattern 1: Pass Through (no bridge needed)
Section titled “Pattern 1: Pass Through (no bridge needed)”When: the library already classified the error. Enrich it with oops and let classification travel the Unwrap chain:
order, err := store.GetOrder(orderID) // library returns NewRejection(...)if err != nil { enriched := oops.In("checkout"). Trace(traceID). With("user_id", userID). Wrap(err) // oops enrichment wraps the classified error
return enriched // Classify() finds Rejection through the chain}No bridge call is needed because Classify walks Unwrap chains: the oops wrapper’s Unwrap() returns the library’s *Error, which implements Classified. This is the most common case — the bridge is not a mandatory pipeline stage.
Pattern 2: AutoWrap (oops-first, bridge infers the family)
Section titled “Pattern 2: AutoWrap (oops-first, bridge infers the family)”When: the application creates its own errors with oops and wants the family inferred from oops metadata:
rich := oops.In("validation"). Tags("rejection"). Code("order.id_format"). With("received", orderID). Errorf("invalid order ID format")
return bridge.AutoWrap(rich) // infers RejectionThe inference cascade is: tags (developer-intentional: retryable, transient, conflict, corruption, rejection, infrastructure) → domain (structural: validation/auth → Rejection, database/network → Transient, storage → Infrastructure, data/schema → Corruption) → Transient (fail-open, same default as Classify on unknown errors).
Pattern 3: Explicit Wrap (application knows the family)
Section titled “Pattern 3: Explicit Wrap (application knows the family)”When: the application knows the exact family and wants full control:
rich := oops.In("checkout"). Code("checkout.inventory_blocked"). Trace(traceID). Wrap(invErr)
return bridge.Wrap(rich, errorfamily.Conflict) // explicit familybridge.Wrap attaches the Family while preserving the full OopsError — so the same error still renders stack traces and context for operators.
One Error, Two Representations
Section titled “One Error, Two Representations”The value of enrich-then-classify: the same error object serves two audiences.
| Audience | What they see | How |
|---|---|---|
| Operator | Stack trace, trace ID, every context field | logger.Error(fmt.Sprintf("%+v", err)) |
| HTTP client | Safe {family, code, message} — no internals |
errorfamily.HTTPHandler |
The raw err.Error() is never sent to the client — HTTPHandler responds with family, code, and optionally a registered MessageTemplate.
Decision Guide
Section titled “Decision Guide”| Situation | Use |
|---|---|
| Library already classified the error | oops.Wrap(err) — done |
| Application creates errors with oops | bridge.AutoWrap(err) |
| Application knows the exact family | bridge.Wrap(err, family) |
| Need just the family from an oops error | bridge.InferFamily(err) |
| Don’t use oops at all | errorfamily.New* / Wrap* |
Reference Implementation
Section titled “Reference Implementation”The full runnable version lives in examples/cmd/bridge, with examples/checkout as the classified library layer and tests proving each pattern end-to-end.
See Classification for how Classify resolves families through wrapper chains, and HTTP & CLI Boundaries for what happens at the boundary.