Skip to content

Error Handling

Queuert provides only job completion — there is no built-in “failure” state. This is intentional: you control how errors are represented in your job outputs.

Return error information in your output type. The caller inspects the output to determine success or failure.

const jobTypeRegistry = defineJobTypeRegistry<{
"process-payment": {
entry: true;
input: { orderId: string };
output: { success: true; transactionId: string } | { success: false; error: string };
};
}>();

For workflows that need rollback, continue to a compensation job that undoes previous steps.

const jobTypeRegistry = defineJobTypeRegistry<{
"charge-card": {
entry: true;
input: { orderId: string };
continueWith: { typeName: "ship-order" | "refund-charge" };
};
"ship-order": {
input: { orderId: string; chargeId: string };
output: { shipped: true };
continueWith: { typeName: "refund-charge" }; // Can continue to refund on failure
};
"refund-charge": {
input: { chargeId: string };
output: { refunded: true };
};
}>();

When a job throws an error, it’s automatically rescheduled with exponential backoff. For transient failures where you want explicit control over retry timing, use rescheduleJob:

import { rescheduleJob } from "queuert";
const worker = await createInProcessWorker({
client,
jobTypeProcessorRegistry: createJobTypeProcessorRegistry({
client,
jobTypeRegistry,
processors: {
"call-external-api": {
attemptHandler: async ({ job, prepare, complete }) => {
const response = await fetch(job.input.url);
if (response.status === 429) {
// Rate limited — retry after the specified delay
const retryAfter = parseInt(response.headers.get("Retry-After") || "60", 10);
rescheduleJob({ afterMs: retryAfter * 1000 });
}
if (!response.ok) {
// Other errors use default exponential backoff
throw new Error(`API error: ${response.status}`);
}
const data = await response.json();
return complete(() => ({ data }));
},
},
},
}),
});
const stop = await worker.start();

rescheduleJob throws a RescheduleJobError which the worker catches specially. Unlike regular errors that trigger exponential backoff based on attempt count, rescheduleJob uses your specified schedule exactly:

rescheduleJob({ afterMs: 30_000 }); // 30 seconds from now
rescheduleJob({ at: new Date("2026-06-15T09:00:00Z") }); // specific time
rescheduleJob({ afterMs: 60_000 }, originalError); // with cause for logging

On retry, job.lastAttemptError contains the serialized error from the previous attempt. Use it for logging or to adjust retry behavior:

attemptHandler: async ({ job, complete }) => {
if (job.lastAttemptError != null) {
console.log(`Previous attempt failed: ${job.lastAttemptError}`);
}
// ...
},
Thrown valueStored as
Error objectStack trace (includes message). Own enumerable properties appended as JSON.
Plain objectJSON-stringified
StringStored as-is

Values are truncated to 10,000 characters.

See examples/showcase-error-handling for a complete working example demonstrating discriminated unions, compensation patterns, and explicit rescheduling. See also Job Processing Reliability for engine-level safety guarantees (savepoints, automatic rollback), Timeouts, and Job Processing Modes.