Cross-app error vocabulary
The problem
You run several services that all surface errors to the same set of clients (web app, mobile, partner APIs). Each service has its own copy of error code strings (PAYMENT_DECLINED, INVENTORY_INSUFFICIENT, …) — out of sync, no autocomplete, the same code means slightly different things in different services.
You want :
- One canonical list of error codes, owned by one repo
- Typed access from every service (autocomplete on codes, autocomplete on parameterized message arguments)
- Documented intent (
why,fix,link) shipping with the type - Versioned : bumping the package is the audit trail of error vocab changes
The full code
This scenario reuses the Catalogs as packages recipe — here's the version oriented around a shared org-wide error vocab.
1. The package
import { defineErrorCatalog } from 'evlog'
export const orgErrors = defineErrorCatalog('org', {
PAYMENT_DECLINED: {
status: 402,
message: 'Payment declined.',
why: 'The issuing bank rejected the charge.',
fix: 'Try a different payment method or contact the bank.',
link: 'https://docs.acme.com/errors/payment-declined',
},
INVENTORY_INSUFFICIENT: {
status: 409,
message: ({ sku, requested, available }: { sku: string; requested: number; available: number }) =>
`Insufficient inventory for ${sku}: ${available}/${requested} available`,
why: 'Not enough stock to fulfill this line item.',
fix: 'Reduce quantity or remove the item from the cart.',
},
RATE_LIMITED: {
status: 429,
message: 'Too many requests.',
why: 'You exceeded the per-minute call quota for this endpoint.',
fix: 'Back off for 30 seconds, then retry.',
},
} as const)
declare module 'evlog' {
interface RegisteredErrorCatalogs {
org: typeof orgErrors
}
}
2. Package layout
{
"name": "@my-org/evlog-errors",
"version": "0.1.0",
"type": "module",
"main": "./dist/index.mjs",
"types": "./dist/index.d.mts",
"exports": {
".": {
"types": "./dist/index.d.mts",
"import": "./dist/index.mjs"
}
},
"files": ["dist"],
"peerDependencies": {
"evlog": "^2"
}
}
3. Consuming it from any service
pnpm add @my-org/evlog-errors
import '@my-org/evlog-errors' // registers the catalog
import { orgErrors } from '@my-org/evlog-errors'
export async function checkout(input: CheckoutInput) {
const stock = await checkInventory(input.lineItems)
if (stock.insufficient.length > 0) {
const item = stock.insufficient[0]
throw orgErrors.INVENTORY_INSUFFICIENT({
sku: item.sku,
requested: item.requested,
available: item.available,
})
}
const charge = await chargeCard(input.payment)
if (!charge.ok) {
throw orgErrors.PAYMENT_DECLINED({ cause: charge.error })
}
}
Every service that depends on @my-org/evlog-errors autocompletes org.INVENTORY_INSUFFICIENT, type-checks the parameters of the message function, and surfaces the documented why / fix / link to clients via parseError().
What it gives you
- Single source of truth — error codes live in one repo, version-controlled
- Autocomplete everywhere —
createError('org.PAYMENT_DECLINED')types both at call sites and inparseError()consumers - Versioned semantics — adding a new error or refining a
fixships as a bump; consumers pick it up - Composable with multiple catalogs — services can also have local catalogs (
local.SHIPPING_UNSUPPORTED) that don't deserve to be shared
Where to go next
- Catalogs as packages — the full reference for the
defineErrorCatalogpackage pattern - Catalogs in evlog — usage of catalogs from the consumer side
- Structured errors — what
why/fix/linkgive you at the HTTP boundary
Compliance audit
A tamper-evident audit pipeline that ships to your secure backend — typed actions via an audit catalog, signed chain optional.
Overview
Package catalogs, drains, enrichers, and framework integrations as reusable npm libraries — same scaffolding pattern for each, type-level discoverability flows transitively to consumers.