introduction
Typed, registry-driven error classes for TypeScript. Branded codes, secret-safe serialization, framework-agnostic, zero dependencies.
Overview
@gentleduck/error builds one error class per subsystem from a plain object literal: a
registry mapping string codes to HTTP status numbers. The class it returns is fully typed:
which codes exist, what metadata each one carries, and whether metadata is required, all
inferred from the registry, with no separate type declaration to keep in sync.
bun add @gentleduck/error
import { createErrorKit, detail, fault } from '@gentleduck/error'
const REGISTRY = {
NOT_FOUND: 404,
RATE_LIMITED: detail<{ retryAfter: number }>(429),
STORAGE_FAILED: fault<{ cause?: string }>(500),
} as const satisfies Record<string, number>
const kit = createErrorKit('AppError', REGISTRY)
export const AppError = kit.ErrorClass
export const throwAppError = kit.throwError
throw new AppError('RATE_LIMITED', { retryAfter: 60 })
// err.status === 429, err.code === 'RATE_LIMITED'
// err.toJSON() -> { ok: false, error: { code: 'RATE_LIMITED', status: 429, retryAfter: 60 } }
Full walkthrough: Getting started.
Why a registry, not a class hierarchy
The usual alternative is one subclass per error kind (NotFoundError extends AppError), which
scales linearly in files and forces every catch site to chain instanceof checks in the right
order. A registry keeps every code, its status, and its metadata shape in one literal that is
also the map a caller reads to know what a subsystem can raise. Object.keys(REGISTRY) is the
exhaustive list, not a guess from grepping extends. Two kits never satisfy each other's
instanceof, even with identical registries, since each call to createErrorKit declares its own
class. Details: Core concepts.
Zero dependencies
packages/duck-error has no runtime dependencies. typescript is an optional peer, needed
only for the types. It runs anywhere JavaScript does: server, browser, edge runtimes. See
Browser and isomorphic usage.
All the docs
Concepts
- Getting started: install, build a registry from one code to three
- Core concepts: the registry as source of truth, one kit per subsystem, construct vs. throw
- Branded types: how
detail/faultdriveArgs,Bare,Meta,Faults - Error codes and status design: naming, HTTP status selection, growing a registry safely
Using it
- Type narrowing:
hasErrorCode,metaOf, exhaustive code-to-behavior maps - Wrapping unknown errors:
asError/rethrowErrorat I/O boundaries - Secret redaction: exactly what
toJSON()strips, and what it doesn't - Testing: asserting codes, proving redaction, pinning exhaustiveness
- Extending: composing multiple kits, decorating responses, subclassing
- Browser and isomorphic usage: client-side gotchas
- FAQ: runtime validation, cloning, stack traces, JavaScript-only usage, and more
Reference
- API reference: every export, every signature, in prose
- API / types: the same surface, generated from source with TypeDoc
Framework integrations