Errors and resources
Two small primitives decide what a handler shows the outside world. defineError makes a failure typed and status-tagged, so the transport renders it correctly. defineResource allow-lists the fields that may leave the building, so an internal record can't leak a column it happens to carry. Both default to safe — you opt into exposure, never out of it.
Errors: throw a typed failure
defineError(meta) returns a value that is both an Error you can throw and a factory that attaches context:
import { defineError } from "@nwire/handler";
export const UserNotFound = defineError({
code: "USER_NOT_FOUND",
status: 404,
summary: "No user found with that id.",
});Throw it bare, or call it to attach context to the thrown clone:
throw UserNotFound; // the definition is itself an Error
throw UserNotFound({ userId: id }); // a contextual cloneThe transport reads code, status, and summary and renders a structured response — for HTTP, a 404 with:
{ "error": { "code": "USER_NOT_FOUND", "summary": "No user found with that id." } }@nwire/handler ships the common ones — Unauthorized (401), Forbidden (403), NotFound (404), Conflict (409), BadRequest (400) — so most handlers throw a built-in and define only their domain-specific failures.
The redaction default
This is the part that matters in production. A throw that isn't a typed defineError — a driver exception, a thrown string, a bare new Error(...) — never reaches the caller verbatim. The transport maps it to a generic { code: "internal_error", status: 500 } and the original is logged server-side, not serialized to the client.
So a Postgres error mentioning a table name, a stack trace, or PII someone attached to an Error can't escape by accident. You'd have to define an error to expose one — exposure is a deliberate act. (An exposeErrors escape hatch exists for local debugging; it's off by default in every environment.)
One mapper enforces this on every surface a caller can reach: wired HTTP routes, Studio's dispatch panel (POST /_nwire/dispatch), and MCP's dispatch_action all render the same { error: { code, summary } } envelope with the same rules — zod failures answer 400 validation_failed with the failing field (the caller's own input, safe to echo), and everything untyped is the opaque internal_error. See the error contract for the full table.
Resources: shape what goes out
A handler often returns more than the caller should see — a user row carries a passwordHash, an order carries internal flags. defineResource declares the public projection: the field allow-list is the contract, and anything not listed never serializes.
The same projection feeds the generated OpenAPI spec, so the documented response shape and the actual response shape are the same object — they can't drift.
See also
defineErrorreference — custom types, status mapping, context.defineResourcereference — projection helpers and OpenAPI emission.- Resources + errors guide — where the error mapper sits in the request pipeline.