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 maps an error id to a message, an HTTP status, and an optional typed context payload.

  • Domain — a named group of related errors (err_user_auth, err_payment).
  • Schema — maps error id strings to message, HTTP status, and optional typed context.
  • Error id — identifies what went wrong; multiple ids can be active on one error.
  • Context — structured data attached to an id (e.g. { username: string }).
  • Hydration — deserializing context from a JSON round-trip back to the original typed value.
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: {},
}),
},
} as const);

The as const on the definition object is what lets TypeScript infer literal ids and context shapes. Don’t omit it.

err() declares one schema entry. Whether context is accepted or required at creation depends on how you call it:

Schema entryfromId("id")fromId("id", ctx)
err() / err({ message, httpStatusCode })
err<C>({ context: {} })✓ optional✓ optional
err<C>({ context: { required: true } })✓ required

message and httpStatusCode can each be a static value or a function of the context.

For context that isn’t JSON-native (an Error, a Date, a Map), attach a serialization pair so it survives a round 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 serialized form travels on the wire; hydration restores the original typed 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>;

Next: Creating & reading errors →