workflow
Version:
Workflow SDK - Build durable, resilient, and observable workflows
85 lines (63 loc) • 4.11 kB
text/mdx
---
title: abort-signal-timeout-in-workflow
description: AbortSignal.timeout() cannot be used inside workflow functions because it relies on real timers which break deterministic replay.
type: troubleshooting
summary: Use sleep() with AbortController instead of AbortSignal.timeout() in workflow functions.
prerequisites:
- /docs/foundations/workflows-and-steps
related:
- /docs/foundations/cancellation
- /docs/api-reference/workflow/sleep
- /docs/errors/timeout-in-workflow
---
<CopyPrompt
text="Replace `AbortSignal.timeout()` inside workflow functions. Find the failing `AbortSignal.timeout(ms)` call in a `"use workflow"` function. Import `sleep` from `workflow`. Create `const controller = new AbortController()`, pass `controller.signal` into the async work that supports cancellation, and start a deterministic timeout with `void sleep("10s").then(() => controller.abort())` or a `Promise.race` between the work and `sleep(...)`. Keep actual fetch/SDK side effects inside `"use step"` helpers when they need Node.js APIs. Treat intentional aborts as non-retryable if appropriate by throwing `FatalError`. Verify the operation succeeds before the timeout, aborts after the timeout, and replays without abort-signal-timeout-in-workflow."
/>
## Error
```text
AbortSignal.timeout() is not supported in workflow functions.
Use sleep() with an AbortController instead.
```
## Why this happens
`AbortSignal.timeout()` creates a signal that aborts after a real-time delay using an internal timer. Workflow functions must be [deterministic](/docs/foundations/workflows-and-steps) to support replay: they run the same code multiple times during the workflow's lifecycle, using the [event log](/docs/how-it-works/event-sourcing) to resume execution to the correct point.
Real-time timers break this determinism because:
- On the first execution, the timer might fire after 10 seconds.
- On replay, the timer would fire again, but the event log may have already advanced past that point.
- The timer's behavior depends on wall-clock time, which varies between executions.
## How to fix
Use [`sleep()`](/docs/api-reference/workflow/sleep) with an `AbortController` to create a deterministic timeout that cancels in-flight work:
**Before (incorrect):**
{/* @skip-typecheck: intentionally incorrect example */}
```typescript lineNumbers
export async function workflow() {
"use workflow";
// This will throw an error
const signal = AbortSignal.timeout(10_000); // [!code highlight]
const result = await fetchData(signal);
return result;
}
```
**After (correct):**
```typescript lineNumbers
import { sleep } from "workflow";
export async function workflow() {
"use workflow";
const controller = new AbortController(); // [!code highlight]
void sleep("10s").then(() => controller.abort()); // [!code highlight]
return await fetchData(controller.signal);
}
async function fetchData(signal: AbortSignal) {
"use step";
const response = await fetch("https://api.example.com/data", { signal });
return response.json();
}
```
The `sleep()` and `AbortController` pattern is the durable equivalent of `AbortSignal.timeout()`. The sleep is recorded in the event log, so it replays deterministically. If `fetchData` finishes within 10 seconds, you get the response. Otherwise, the timer calls `controller.abort()`, `fetch` rejects with an `AbortError`, and the step's failure propagates to the workflow as a `FatalError` (no retries because the abort is intentional cancellation).
<Callout type="info">
`AbortSignal.timeout()` works normally inside step functions, since steps have full Node.js runtime access and are not replayed.
</Callout>
## Related
- [Cancellation](/docs/foundations/cancellation): Patterns for canceling in-flight work
- [`sleep()` API Reference](/docs/api-reference/workflow/sleep): Durable sleep primitive
- [Workflows and Steps](/docs/foundations/workflows-and-steps): Why workflow functions must be deterministic
- [`setTimeout` in Workflow](/docs/errors/timeout-in-workflow): Similar restriction on `setTimeout`