Skip to content
RELKIT
Esc
↑↓navigate↵open⌘Jpreview
On this page

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.

Reference and source