Scenarios

Cross-app error vocabulary

One `@my-org/evlog-errors` package every service depends on — same error codes, same `why` / `fix` strings, type-safe everywhere.

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

src/index.ts
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

package.json
{
  "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
services/checkout/src/handler.ts
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 everywherecreateError('org.PAYMENT_DECLINED') types both at call sites and in parseError() consumers
  • Versioned semantics — adding a new error or refining a fix ships 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