Declared errors
Give expected failures stable data, HTTP behavior, and retry guidance without exposing internal defects.
Declare an error when callers need to handle an expected failure, such as an order that does not exist. An unexpected exception is a defect: RELKIT records it internally and returns a safe boundary failure instead of exposing a stack or provider detail.
Scaffold a typed error
From the application root, with an existing orders service:
bun run relkit add error receipt-rejected --service orders
Creates src/services/orders/errors/receipt-rejected.error.ts with { message: string } data and
retry: "never". It stays internal. Add it to the relevant function’s errors list yourself;
a service bundle containing an error wires it to the bundle’s example function automatically.
For every scaffolding flag, see the error command reference.
Define a stable failure
/** Carries the missing order identity from lookup to an HTTP 404 with the established one-second retry hint. */
import { defineError } from "@relkit/app/functions";
import { z } from "@relkit/app/schema";
const orderNotFound = defineError({
id: "orders.not-found",
data: z.object({
/** Requested order identity preserved in the public failure message and payload. */
orderId: z.string(),
}),
message: ({ orderId }) => `Order ${orderId} was not found`,
http: { status: 404 },
retry: { kind: "later", afterMs: 1_000 },
});
export default orderNotFound;
The id is the durable contract used by graphs, clients, traces, and matching. data limits the details callers
may receive. http.status contributes a typed HTTP response, while retry tells durable delivery whether another
attempt could help.
Register the descriptor in a function’s errors list and throw the constructed declared value. Do not convert
every caught exception into a declared error; callers should depend only on failures the domain promises.
Verify the public contract
The order lookup function uses the declared error when no record exists:
/** Emits lookup progress and returns the demonstration order or its declared order-not-found error. */
import { defineFunction } from "@relkit/app/functions";
import { z } from "@relkit/app/schema";
import orderNotFound from "../errors/order-not-found.error.js";
import { orderLookupInput, orderLookupOutput } from "../../../platform/schemas.js";
const getOrder = defineFunction({
input: orderLookupInput,
output: orderLookupOutput,
progress: z.object({
stage: z.union([z.literal("lookup-started"), z.literal("lookup-completed")]),
}),
errors: [orderNotFound],
handler: async (input, context) => {
await context.progress.emit({ stage: "lookup-started" });
if (input.orderId === "missing") return new orderNotFound(input);
await context.progress.emit({ stage: "lookup-completed" });
return { orderId: input.orderId, status: "confirmed", totalCents: 1_000 };
},
});
export default getOrder;
bun run check
curl -i http://localhost:3000/orders/missing
Expect HTTP 404 with the stable error code and validated data. Tests should assert those fields and the status,
not a complete internal message or stack trace.
Treat retry as bounded guidance
"never"means the same operation should not be retried automatically."later"allows a durable transport to use its normal backoff.{ kind: "later", afterMs }supplies a minimum delay; the integration may wait longer.
HTTP and direct invocation do not automatically retry. Jobs and event delivery apply their own bounded policies, so a retryable error never means an unbounded loop. See task retries and Event delivery for transport behavior.