UNPKG

workflow

Version:

Workflow SDK - Build durable, resilient, and observable workflows

228 lines (169 loc) • 6.92 kB
--- title: Errors & Retrying description: Customize retry behavior with FatalError and RetryableError for robust error handling. type: conceptual summary: Control how steps handle failures and customize retry behavior. prerequisites: - /docs/foundations/workflows-and-steps related: - /docs/api-reference/workflow/fatal-error - /docs/api-reference/workflow/retryable-error --- By default, errors thrown inside steps are retried. Additionally, Workflow SDK provides two new types of errors you can use to customize retries. ## Default Retrying By default, steps retry up to 3 times on arbitrary errors. You can customize the number of retries by adding a `maxRetries` property to the step function. ```typescript lineNumbers async function callApi(endpoint: string) { "use step"; const response = await fetch(endpoint); if (response.status >= 500) { // Any uncaught error gets retried throw new Error("Uncaught exceptions get retried!"); // [!code highlight] } return response.json(); } callApi.maxRetries = 5; // Retry up to 5 times on failure (6 total attempts) ``` Steps get enqueued immediately after a failure. Read on to see how this can be customized. <Callout type="info"> When a retried step performs external side effects (payments, emails, API writes), ensure those calls are <strong>idempotent</strong> to avoid duplicate side effects. See <a href="/docs/foundations/idempotency">Idempotency</a> for more information. </Callout> ## Intentional Errors When your step needs to intentionally throw an error and skip retrying, simply throw a [`FatalError`](/docs/api-reference/workflow/fatal-error). ```typescript lineNumbers import { FatalError } from "workflow"; async function callApi(endpoint: string) { "use step"; const response = await fetch(endpoint); if (response.status >= 500) { // Any uncaught error gets retried throw new Error("Uncaught exceptions get retried!"); } if (response.status === 404) { throw new FatalError("Resource not found. Skipping retries."); // [!code highlight] } return response.json(); } ``` ## Customize Retry Behavior When you need to customize the delay on a retry, use [`RetryableError`](/docs/api-reference/workflow/retryable-error) and set the `retryAfter` property. ```typescript lineNumbers import { FatalError, RetryableError } from "workflow"; async function callApi(endpoint: string) { "use step"; const response = await fetch(endpoint); if (response.status >= 500) { throw new Error("Uncaught exceptions get retried!"); } if (response.status === 404) { throw new FatalError("Resource not found. Skipping retries."); } if (response.status === 429) { throw new RetryableError("Rate limited. Retrying...", { // [!code highlight] retryAfter: "1m", // Duration string // [!code highlight] }); // [!code highlight] } return response.json(); } ``` ## Advanced Example This final example combines everything we've learned, along with [`getStepMetadata`](/docs/api-reference/workflow/get-step-metadata). ```typescript lineNumbers import { FatalError, RetryableError, getStepMetadata } from "workflow"; async function callApi(endpoint: string) { "use step"; const metadata = getStepMetadata(); const response = await fetch(endpoint); if (response.status >= 500) { // Exponential backoffs throw new RetryableError("Backing off...", { retryAfter: (metadata.attempt ** 2) * 1000, // [!code highlight] }); } if (response.status === 404) { throw new FatalError("Resource not found. Skipping retries."); } if (response.status === 429) { throw new RetryableError("Rate limited. Retrying...", { retryAfter: new Date(Date.now() + 60000), // Date instance // [!code highlight] }); } return response.json(); } callApi.maxRetries = 5; // Retry up to 5 times on failure (6 total attempts) ``` <Callout type="info"> Setting <code>maxRetries = 0</code> means the step will run once but will not be retried on failure. The default is <code>maxRetries = 3</code>, meaning the step can run up to 4 times total (1 initial attempt + 3 retries). </Callout> ## Error Codes When a workflow run fails, the error may include a `code` that classifies the failure. You can access it programmatically via the `Run` class: ```typescript lineNumbers import { WorkflowRunFailedError } from "@workflow/errors"; import { start } from "workflow/api"; const run = await start(myWorkflow, [input]); try { const result = await run.returnValue; } catch (err) { if (WorkflowRunFailedError.is(err)) { console.log(err.cause.code); // "USER_ERROR", "RUNTIME_ERROR", or undefined console.log(err.cause.message); // The error message } } ``` | Code | Meaning | | --- | --- | | `USER_ERROR` | An error thrown in your workflow or step code (including propagated step failures like `FatalError`) | | `RUNTIME_ERROR` | An internal runtime error such as a corrupted event log or missing data. If you see this, please [file an issue](https://github.com/vercel/workflow/issues) | <Callout type="info"> The error code is also available on the run entity via the CLI (`npx workflow inspect runs <runId>`) in the `error.code` field, and as an OTEL span attribute (`workflow.error.code`) for observability. </Callout> ## Rolling Back Failed Steps When a workflow fails partway through, it can leave the system in an inconsistent state. A common pattern to address this is "rollbacks": for each successful step, record a corresponding rollback action that can undo it. If a later step fails, run the rollbacks in reverse order to roll back. Key guidelines: - Make rollbacks steps as well, so they are durable and benefit from retries. - Ensure rollbacks are [idempotent](/docs/foundations/idempotency); they may run more than once. - Only enqueue a compensation after its forward step succeeds. ```typescript lineNumbers // Forward steps async function reserveInventory(orderId: string) { "use step"; // ... call inventory service to reserve ... } async function chargePayment(orderId: string) { "use step"; // ... charge the customer ... } // Rollback steps async function releaseInventory(orderId: string) { "use step"; // ... undo inventory reservation ... } async function refundPayment(orderId: string) { "use step"; // ... refund the charge ... } export async function placeOrderSaga(orderId: string) { "use workflow"; const rollbacks: Array<() => Promise<void>> = []; try { await reserveInventory(orderId); rollbacks.push(() => releaseInventory(orderId)); await chargePayment(orderId); rollbacks.push(() => refundPayment(orderId)); // ... more steps & rollbacks ... } catch (e) { for (const rollback of rollbacks.reverse()) { await rollback(); } // Rethrow so the workflow records the failure after rollbacks throw e; } } ```