Skip to content

Error Domains

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.

  • 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.
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: {},
}),
},
});

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 itfromId("id")fromId("id", ctx)
err() / err({ message, httpStatusCode }) — no context
err<C>({ context: {} }) — optional context✓ optional✓ optional
err<C>({ 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.

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:

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 unpacks it back into the real value on the other side.

import type { InferNiceError, InferNiceErrorHydrated } from "@nice-code/error";
type TAuthError = InferNiceError<typeof err_auth>;
type TAuthErrorHydrated = InferNiceErrorHydrated<typeof err_auth>;