Skip to content

@nice-code/error

Declare error domains once, then throw, catch, serialize, and narrow them with full type inference. Multi-ID errors, pattern matching, and safe transport across any boundary.

@nice-code/action

Wrap server functions as actions with typed input, output, and errors. Requesters on the client, resolvers on the server — no codegen.

@nice-code/common-errors

Shared error domains. Standard Schema validation errors and Hono middleware, ready to drop in.

Errors round-trip through JSON.stringify and come back as the same typed error — via toJsonObject() on the way out, castNiceError() on the way in.

Each error ID declares its context shape. TypeScript narrows on .hasId() and .hasOneOfIds(), giving you typed context access with no casts.

handleWithSync and handleWithAsync route errors to the first matching case. forDomain, forId, forIds — no switch statements needed.

Node, Bun, Deno, browsers, service workers, edge runtimes, Cloudflare Workers. No environment assumptions.

errors.ts
import { defineNiceError, err } from "@nice-code/error"
export const err_billing = defineNiceError({
domain: "err_billing",
schema: {
payment_failed: err<{ reason: string }>({
message: ({ reason }) => `Payment failed: ${reason}`,
httpStatusCode: 402,
context: { required: true },
}),
card_expired: err({ message: "Card has expired", httpStatusCode: 402 }),
},
})
charge.ts
import { err_billing } from "./errors"
export async function charge(amount: number) {
const result = await stripe.charge(amount)
if (!result.ok) {
throw err_billing.fromId("payment_failed", { reason: result.message })
}
}
handle.ts
import { castNiceError, forDomain } from "@nice-code/error"
import { err_billing } from "./errors"
try {
await charge(500)
} catch (e) {
const error = castNiceError(e)
error.handleWithSync([
forDomain(err_billing, (h) => {
if (h.hasId("payment_failed")) {
showError(h.getContext("payment_failed").reason)
}
}),
])
}