From f2dc07748d0822e7ab2c6d59899e24d7d867e069 Mon Sep 17 00:00:00 2001 From: Ethan Niser Date: Fri, 24 Oct 2025 20:26:33 -0700 Subject: [PATCH 1/2] rollback docs --- .../docs/foundations/errors-and-retries.mdx | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/docs/content/docs/foundations/errors-and-retries.mdx b/docs/content/docs/foundations/errors-and-retries.mdx index 090c4310f..674b7b538 100644 --- a/docs/content/docs/foundations/errors-and-retries.mdx +++ b/docs/content/docs/foundations/errors-and-retries.mdx @@ -132,3 +132,61 @@ async function callApi(endpoint: string) { } callApi.maxRetries = 5; ``` + +## Compensating Actions + +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> = []; + + 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; + } +} +``` From abf77b7a514e414cb9a3b81a45c8c4c2ec358c5d Mon Sep 17 00:00:00 2001 From: Nathan Rajlich Date: Fri, 24 Oct 2025 23:16:41 -0700 Subject: [PATCH 2/2] Apply suggestion from @TooTallNate --- docs/content/docs/foundations/errors-and-retries.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/docs/foundations/errors-and-retries.mdx b/docs/content/docs/foundations/errors-and-retries.mdx index 674b7b538..c73cb5d53 100644 --- a/docs/content/docs/foundations/errors-and-retries.mdx +++ b/docs/content/docs/foundations/errors-and-retries.mdx @@ -133,7 +133,7 @@ async function callApi(endpoint: string) { callApi.maxRetries = 5; ``` -## Compensating Actions +## 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.