# @nice-code/error — documentation Errors you can type, send over the network, and match on by name. This file concatenates only the @nice-code/error pages of https://nicecode.io — point a local AI coding assistant at it when you're working with just this package. For the full set (every nice-code package and how they fit together) use /llms.txt. Generated from src/content/docs — do not edit by hand. --- # Creating & Reading Source: /nice-error/creating-reading Description: Create single- and multi-id errors, then read and narrow them by id. ## Creating errors ```ts // Single id — context required per schema const err1 = err_auth.fromId("invalid_credentials", { username: "alice" }); // Multi-id in one error — several things went wrong at once const err2 = err_auth.fromContext({ invalid_credentials: { username: "bob" }, rate_limited: { retryAfter: 30 }, }); // Chain additional ids after construction const err3 = err_auth.fromId("account_locked").addId("rate_limited"); ``` A single `NiceError` can carry **multiple active ids**. This models real situations — a request that is both unauthenticated _and_ rate-limited — without forcing you to pick one. ## Reading and narrowing ```ts // Check and narrow — inside the guard, getContext is fully typed if (err1.hasId("invalid_credentials")) { const { username } = err1.getContext("invalid_credentials"); } // Check several ids at once — narrows to that subset if (err2.hasOneOfIds(["invalid_credentials", "rate_limited"])) { // ... } // All active ids err2.getIds(); // ["invalid_credentials", "rate_limited"] ``` ## Pattern matching `matchFirst` runs the first handler whose id is active and returns its result. Provide `_` as a fallback. ```ts import { matchFirst } from "@nice-code/error"; const message = matchFirst(err1, { invalid_credentials: ({ username }) => `Wrong password for ${username}`, account_locked: () => "Account locked", _: () => "Unknown auth error", }); ``` ## Instance properties ```ts const e = err_auth.fromId("invalid_credentials", { username: "alice" }); e.domain; // "err_auth" e.message; // "Invalid credentials for: alice" e.httpStatusCode; // 401 e.getIds(); // ["invalid_credentials"] ``` --- # Error Domains Source: /nice-error/domains Description: Declare typed error schemas with defineNiceError and the err() helper. An **error domain** is a named group of related errors. Each entry in its **schema** ties an error id to a message, an HTTP status, and (optionally) some typed extra data called **context**. ## The words we use - **Domain** — a named group of related errors (`err_auth`, `err_payment`). - **Schema** — the list that maps each error id to its message, HTTP status, and optional context. - **Error id** — a short name for what went wrong. One error can have several ids active at once. - **Context** — extra data attached to an id (e.g. `{ username: string }`). - **Hydration** — when an error has travelled as JSON, hydration turns its context back into the real typed values it started as. ## Defining a domain ```ts import { defineNiceError, err } from "@nice-code/error"; export const err_auth = defineNiceError({ domain: "err_auth", schema: { // No context — second arg to fromId is not accepted account_locked: err({ message: "Account is locked", httpStatusCode: 403, }), // Required context — second arg to fromId is required invalid_credentials: err<{ username: string }>({ message: ({ username }) => `Invalid credentials for: ${username}`, httpStatusCode: 401, context: { required: true }, }), // Optional context rate_limited: err<{ retryAfter: number }>({ message: (ctx) => (ctx ? `Retry after ${ctx.retryAfter}s` : "Rate limited"), httpStatusCode: 429, context: {}, }), }, }); ``` ## The `err()` helper `err()` describes one error in the schema. How you call it decides whether context is allowed — or required — when you later create the error: | How you declare it | `fromId("id")` | `fromId("id", ctx)` | |---|---|---| | `err()` / `err({ message, httpStatusCode })` — no context | ✓ | ✗ | | `err({ context: {} })` — optional context | ✓ optional | ✓ optional | | `err({ context: { required: true } })` — required context | ✗ | ✓ required | `message` and `httpStatusCode` can each be a fixed value, or a function that builds the value from the context. ## Sending context that isn't plain JSON If a context value isn't something JSON can carry on its own (an `Error`, a `Date`, a `Map`), give it a pair of functions — one to pack it, one to unpack it — so it survives the trip: ```ts fs_error: err<{ cause: NodeJS.ErrnoException }>({ message: ({ cause }) => `FS error: ${cause.message}`, context: { required: true, serialization: { toJsonSerializable: ({ cause }) => ({ code: cause.code, message: cause.message }), fromJsonSerializable: (obj) => ({ cause: Object.assign(new Error(obj.message), obj) }), }, }, }), ``` The packed form is what travels over the network; [hydration](/nice-error/serialization/) unpacks it back into the real value on the other side. ## Utility types ```ts import type { InferNiceError, InferNiceErrorHydrated } from "@nice-code/error"; type TAuthError = InferNiceError; type TAuthErrorHydrated = InferNiceErrorHydrated; ``` --- # Handling Errors Source: /nice-error/handling Description: Route errors with composable handlers or a reusable handler class. nice-code offers two handling styles: a composable functional API, and a reusable class. ## Functional style Build a list of cases with `forId`, `forIds`, and `forDomain`. `handleWithSync` runs the first matching case and returns whether anything handled it. ```ts import { forDomain, forId, forIds } from "@nice-code/error"; const handled = error.handleWithSync([ forId(err_auth, "invalid_credentials", (h) => { const { username } = h.getContext("invalid_credentials"); return res.status(401).json({ error: `Bad credentials for ${username}` }); }), forIds(err_auth, ["account_locked", "rate_limited"], (h) => { return res.status(h.httpStatusCode).json({ error: h.message }); }), forDomain(err_payment, (h) => { return res.status(500).json({ error: "Payment failed" }); }), ]); ``` The handler argument (`h`) is narrowed to the matched id(s), so `getContext` is typed inside each case. ### Async handlers ```ts await error.handleWithAsync([ forDomain(err_payment, async (h) => { await db.logFailure(h.message); await notify(h.toJsonObject()); }), ]); ``` ## Class-based style `NiceErrorHandler` is a reusable handler you configure once and apply to many errors — ideal for a central error boundary. ```ts import { NiceErrorHandler } from "@nice-code/error"; const handler = new NiceErrorHandler() .forId(err_auth, "invalid_credentials", (h) => "unauthorized") .forDomain(err_payment, (h) => "payment_error") .setDefaultHandler((e) => "unknown_error"); handler.handleErrorWithPromiseInspection(error); ``` ## Choosing a style - **Functional** — local, one-off routing inside a single catch block. - **Class-based** — a shared, reusable policy you register once (server middleware, a global boundary). --- # Domain Hierarchies Source: /nice-error/hierarchy Description: Compose domains into parent/child trees and match across ancestry. Domains can nest. A child domain inherits its place in the tree, so a handler can match a broad parent without enumerating every child's ids. ## Creating child domains ```ts const err_app = defineNiceError({ domain: "err_app", schema: {} }); const err_auth = err_app.createChildDomain({ domain: "err_auth", schema: { /* ... */ }, }); const err_auth_registration = err_auth.createChildDomain({ domain: "err_auth_registration", schema: { /* ... */ }, }); ``` ## Ancestry checks ```ts err_app.isParentOf(err_auth_registration); // true err_auth.isParentOf(err_auth_registration); // true err_auth_registration.isParentOf(err_auth); // false ``` ## Type guards ```ts // Exact domain match only err_auth.isExact(someError); // true only for the err_auth domain // This domain OR any descendant err_auth.isThisOrChild(someError); // true for err_auth and all its children ``` Use `isExact` when you want to handle exactly one domain's errors; use `isThisOrChild` to catch a whole subtree — e.g. "any auth-related error" regardless of which specific child domain produced it. ## When to nest - Group errors by bounded context: `err_app` → `err_auth` → `err_auth_registration`. - Let middleware match broadly (`err_auth.isThisOrChild`) while leaf code matches precisely (`err_auth_registration.isExact`). - Keep domain strings stable — they're part of the serialized identity. --- # Packing Source: /nice-error/packing Description: Smuggle a full error through transports that only preserve message or cause. Some layers — certain RPC systems, the boundary into a Cloudflare Durable Object, job queues — only pass along an error's `message` (or `cause`) and throw away every other property. **Packing** tucks the whole error into one of those fields that *do* survive, so you can rebuild it on the other side. ## Packing ```ts import { EErrorPackType } from "@nice-code/error"; // `pack` MUTATES the error in place — it rewrites the chosen field (message/cause) on // THIS instance and returns the same object (not a clone). The original value is stashed // internally so `unpack` can restore it. error.pack(EErrorPackType.msg_pack); // embeds the JSON into error.message error.pack(EErrorPackType.cause_pack); // embeds the JSON into error.cause ``` Throw the packed error across the boundary just like any normal `Error`. ## Unpacking ```ts error.unpack(); // restore the original NiceError ``` In practice you rarely call `unpack` yourself — [`castNiceError`](/nice-error/serialization/) notices a packed error and unpacks it for you: ```ts import { castNiceError } from "@nice-code/error"; const restored = castNiceError(caught); // unpacks if it was packed ``` ## When to reach for it - A **Durable Object** only re-throws `message` when you call into it — pack with `msg_pack` before throwing inside the DO. - A job queue only keeps the error's message — pack before adding the job, then `castNiceError` after pulling it off. - Any layer where you don't control how things are serialized and only the standard `Error` fields make it through. For boundaries you _do_ control (your own HTTP responses), just use [`toJsonObject()` + `castNiceError`](/nice-error/serialization/) instead — packing is only for the ones you don't. --- # Results & Observability Source: /nice-error/results-observability Description: Turn throwing code into typed results, and tap every NiceError for logging. Two things that work well together: a result type for running code that might fail without `try`/`catch`, and a single place to hook into so every `NiceError` can be sent to your logger or telemetry. ## Running code that might fail — `niceTry` `niceTry` / `niceTryAsync` run a function that might throw and hand you back a typed result. If it fails, the error is **always** a `NiceError`. ```ts import { niceTry, niceTryAsync, type TNiceResult } from "@nice-code/error"; const r = niceTry(() => JSON.parse(input)); if (!r.ok) report(r.error); // r.error is a NiceError (isUnhandled === true for a raw throw) else use(r.output); const r2 = await niceTryAsync(() => fetchUser(id)); // accepts a sync or async fn ``` Behind the scenes the `catch` runs through [`castNiceError`](/nice-error/serialization/): a thrown domain error comes through as-is, while anything else is wrapped into an `isUnhandled` error. ### `TNiceResult` and its builders ```ts type TNiceResult = | { ok: true; output: OUT } | { ok: false; error: ERR }; ``` Build results by hand with `niceOk` / `niceErr` when a function returns a `TNiceResult` directly: ```ts import { niceOk, niceErr } from "@nice-code/error"; function parsePort(raw: string): TNiceResult { const n = Number(raw); return Number.isInteger(n) ? niceOk(n) : niceErr(err_config.fromId("bad_port", { raw })); } ``` > `@nice-code/action`'s action outcome is just a `TNiceResult` with one extra field (`expected`), so the two > fit together neatly — see [Error Handling](/nice-action/error-handling/). ## Watching every error `onNiceError` runs every time a `NiceError` is created — send those to Sentry, OpenTelemetry, or your devtools. ```ts import { onNiceError, setNiceErrorLogger } from "@nice-code/error"; const off = onNiceError((error) => report(error.toStructuredLog())); // ...later off(); // remove the tap ``` `error.toStructuredLog()` returns a flat, log-friendly object distinct from `toJsonObject()` (the wire form): ```ts { domain, ids, message, httpStatusCode, isUnhandled, timeCreated, originError? } ``` ### Routing the library's own diagnostics The library's internal warnings default to `console`. Redirect them through your own logger: ```ts setNiceErrorLogger({ warn: myLogger.warn, debug: myLogger.debug, error: myLogger.error }); ``` ## Internal subpath The main `@nice-code/error` surface is all most apps need. The lower-level helpers — inspecting the wire format, the `err_cast_not_nice` fallback domain, `isNiceErrorObject`, context-state types — sit behind a separate subpath, meant for libraries built on top of `nice-error`: ```ts import { isNiceErrorObject, err_cast_not_nice } from "@nice-code/error/internal"; ``` --- # Serialization & Transport Source: /nice-error/serialization Description: Send typed errors across HTTP and reconstruct them with full types. A `NiceError` turns into plain JSON, travels over the network, and is rebuilt on the other side — with its ids, context, and HTTP status all intact. ## Serializing ```ts const json = error.toJsonObject(); // plain JSON object — safe over HTTP const str = error.toJsonString(); // JSON string const response = error.toHttpResponse(); // a Response with the correct HTTP status ``` ## Receiving — `castNiceError` On the receiving side, `castNiceError` turns _any_ caught value into a `NiceError`: ```ts import { castNiceError } from "@nice-code/error"; const caught = castNiceError(unknownValue); // always returns a NiceError if (err_auth.isExact(caught)) { const hydrated = err_auth.hydrate(caught); // deserialize context const { username } = hydrated.getContext("invalid_credentials"); } ``` `castNiceError` copes with anything you hand it — a `NiceError`, a JSON object, a plain `Error`, an error-like object, `null`/`undefined`, even a primitive — and it never throws. ## One-step — `castAndHydrate` When you have a specific domain in mind, `castAndHydrate` casts, checks the domain, and hydrates in a single call: ```ts import { castAndHydrate, matchFirst } from "@nice-code/error"; const error = castAndHydrate(unknownValue, err_auth); // → a hydrated NiceError when it belongs to err_auth, otherwise the raw cast NiceError if (err_auth.isExact(error)) { const message = matchFirst(error, { invalid_credentials: ({ username }) => `Wrong password for ${username}`, account_locked: () => "Account locked", }); } ``` ## Why hydration matters Only JSON travels over the network. So if an id's context held a `Date`, an `Error`, or anything with a [custom pack/unpack pair](/nice-error/domains/#sending-context-that-isnt-plain-json), **hydration** is the step that turns the JSON form back into the real typed value. Always `hydrate` (or `castAndHydrate`) before reading context that just came in from somewhere else. ## A full round trip ```ts // Server return error.toHttpResponse(); // Client const caught = castNiceError(await res.json()); if (err_auth.isExact(caught)) { const hydrated = err_auth.hydrate(caught); // hydrated.getContext(...) is now fully typed and real } ```