Structured Errors
The errors your package throws cross a boundary: the host catches them, serializes them, and decides what the user sees. A bare Error gives the host nothing to work with, so the response comes back as an opaque 500, the support ticket starts with "something failed", and the dashboard chart counting your failures does not exist. A catalog replaces that negotiation with data.
initLogger() belongs to the host application. Nothing on this page touches configuration: a catalog is a module of error factories, and throwing one is what makes the host's pipeline useful. Emitting Events covers the configuration boundary.Define a catalog with a prefix your package owns
defineErrorCatalog builds one factory per entry. The prefix is the first half of the wire format, so give it your package name:
import { defineErrorCatalog } from 'evlog'
export const errors = defineErrorCatalog('mylib', {
RATE_LIMITED: {
status: 429,
message: 'Too many requests',
why: 'The upstream provider returned HTTP 429',
fix: 'Retry with exponential backoff',
link: 'https://mylib.dev/errors/rate-limited',
},
INSUFFICIENT_FUNDS: {
status: 402,
message: ({ available, required }: { available: number, required: number }) =>
`Insufficient funds: $${available}/$${required}`,
},
})
Each entry accepts: status (default 500), message as a constant string or a typed function whose params become required factory arguments, why, fix, link, tags, and internal for backend-only context. The Structured Errors page documents the full anatomy as an application sees it.
The wire format is ${prefix}.${KEY}: throw errors.RATE_LIMITED() and the host receives an error whose code is mylib.RATE_LIMITED, with status, message, why, and fix attached. One prefix per package is what keeps a graph coherent: a host running several evlog-instrumented packages never sees two codes collide, because each package owns one namespace, the way each package owns one npm name.
Throw and let the host route it
The factory returns an EvlogError. Throwing it is the whole integration:
import { errors } from './errors'
export function charge(amount: number): void {
if (amount <= 0) {
throw errors.INSUFFICIENT_FUNDS({ available: amount, required: 1 })
}
// ...
}
What the host gets: a status it can put on the HTTP response, a code its drain can index, and why/fix it can show. internal stays backend-only: it is omitted from JSON.stringify(error) and from every framework serializer, so it is the right place for context you would not send to a client.
Hosts should catch with EvlogError.isEvlogError(error) rather than instanceof. Package managers routinely install more than one physical copy of evlog, and instanceof silently reports false across copies, which downgrades your structured error to a bare 500. The brand check works across copies. Document this in your package's error handling guide if you document throwing behavior.
Publish the catalog as its own entrypoint
Put the catalog in src/errors.ts and export it under its own subpath, so hosts can import your codes without pulling the rest of the package:
{
"exports": {
".": "./dist/index.mjs",
"./errors": "./dist/errors.mjs"
},
"peerDependencies": {
"evlog": "^2.0.0"
}
}
evlog stays a peer dependency: the catalog factories build EvlogError instances from whatever copy of evlog the host resolved, so your errors and the host's share one type. If your package is large enough to need several catalogs, the npm packaging recipe on Catalogs covers splitting them per bounded context.
Register the catalog in your package's type surface so your consumers get autocomplete on the codes:
declare module 'evlog' {
interface RegisteredErrorCatalogs {
mylib: typeof errors
}
}
Treat error codes as wire format
The code string crosses your package boundary and lands in the host's dashboards, alerts, and support macros. Renaming a code is a breaking change, the same way renaming an exported function is: anyone matching on mylib.RATE_LIMITED breaks. Add codes freely, reorder entries safely, and when a code must go, deprecate before you remove it. Testing shows how to make a rename fail in your own test suite before it fails in a host.
Emitting Events
Emit events from library code the host can attribute and filter: the global log API, source fields, request participation, and duplicate installs.
Testing
Test the events your package emits and the errors it throws: a collecting drain, the memory drain, and factory codes that fail on rename.