Skip to content

Commit b56aae3

Browse files
CopilotTooTallNatepranaygp
authored
Override timeout functions in workflow VM context to throw helpful errors (#505)
* Initial plan * Override timeout functions in workflow VM context to throw helpful errors Co-authored-by: TooTallNate <[email protected]> * Use WorkflowRuntimeError instead of vmGlobalThis.Error for timeout functions Co-authored-by: pranaygp <[email protected]> * Add docs page for timeout-in-workflow error Co-authored-by: pranaygp <[email protected]> * Reorder unavailable functions list: set* functions first, then clear* functions Co-authored-by: TooTallNate <[email protected]> --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: TooTallNate <[email protected]> Co-authored-by: pranaygp <[email protected]>
1 parent 3198f6d commit b56aae3

File tree

6 files changed

+381
-1
lines changed

6 files changed

+381
-1
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@workflow/errors": patch
3+
"@workflow/core": patch
4+
---
5+
6+
Override setTimeout, setInterval, and related functions in workflow VM context to throw helpful errors suggesting to use `sleep` instead

docs/content/docs/errors/index.mdx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ Fix common mistakes when creating and executing workflows in the **Workflow DevK
1717
<Card href="/docs/errors/start-invalid-workflow-function" title="start-invalid-workflow-function">
1818
Learn how to start an invalid workflow function.
1919
</Card>
20+
<Card href="/docs/errors/timeout-in-workflow" title="timeout-in-workflow">
21+
Learn how to handle timing delays in workflow functions.
22+
</Card>
2023
<Card href="/docs/errors/webhook-invalid-respond-with-value" title="webhook-invalid-respond-with-value">
2124
Learn how to use the correct `respondWith` values for webhooks.
2225
</Card>
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
---
2+
title: timeout-in-workflow
3+
---
4+
5+
This error occurs when you try to use `setTimeout()`, `setInterval()`, or related timing functions directly inside a workflow function.
6+
7+
## Error Message
8+
9+
```
10+
Timeout functions like "setTimeout" and "setInterval" are not supported in workflow functions. Use the "sleep" function from "workflow" for time-based delays.
11+
```
12+
13+
## Why This Happens
14+
15+
Workflow functions run in a sandboxed environment where timing functions like `setTimeout()` and `setInterval()` are not available. These functions rely on asynchronous scheduling that would break the **deterministic replay** guarantees that workflows depend on.
16+
17+
When a workflow suspends and later resumes, it replays from the event log. If timing functions were allowed, the replay would produce different results than the original execution.
18+
19+
## Quick Fix
20+
21+
Use the `sleep` function from the `workflow` package for time-based delays. Unlike `setTimeout()`, `sleep` is tracked in the event log and replays correctly.
22+
23+
**Before:**
24+
25+
```typescript lineNumbers title="workflows/delayed.ts"
26+
export async function delayedWorkflow() {
27+
"use workflow";
28+
29+
// Error - setTimeout is not available in workflow functions
30+
await new Promise(resolve => setTimeout(resolve, 5000)); // [!code highlight]
31+
32+
return 'done';
33+
}
34+
```
35+
36+
**After:**
37+
38+
```typescript lineNumbers title="workflows/delayed.ts"
39+
import { sleep } from 'workflow'; // [!code highlight]
40+
41+
export async function delayedWorkflow() {
42+
"use workflow";
43+
44+
// sleep is tracked in the event log and replays correctly
45+
await sleep('5s'); // [!code highlight]
46+
47+
return 'done';
48+
}
49+
```
50+
51+
## Unavailable Functions
52+
53+
These timing functions cannot be used in workflow functions:
54+
55+
- `setTimeout()`
56+
- `setInterval()`
57+
- `setImmediate()`
58+
- `clearTimeout()`
59+
- `clearInterval()`
60+
- `clearImmediate()`
61+
62+
## Common Scenarios
63+
64+
### Polling with Delays
65+
66+
If you need to poll an external service with delays between requests:
67+
68+
```typescript lineNumbers title="workflows/polling.ts"
69+
import { sleep } from 'workflow';
70+
71+
export async function pollingWorkflow() {
72+
"use workflow";
73+
74+
let status = 'pending';
75+
76+
while (status === 'pending') {
77+
status = await checkStatus(); // step function
78+
if (status === 'pending') {
79+
await sleep('10s'); // [!code highlight]
80+
}
81+
}
82+
83+
return status;
84+
}
85+
86+
async function checkStatus() {
87+
"use step";
88+
const response = await fetch('https://api.example.com/status');
89+
const data = await response.json();
90+
return data.status;
91+
}
92+
```
93+
94+
### Scheduled Delays
95+
96+
For workflows that need to wait for a specific duration:
97+
98+
```typescript lineNumbers title="workflows/reminder.ts"
99+
import { sleep } from 'workflow';
100+
101+
export async function reminderWorkflow(message: string) {
102+
"use workflow";
103+
104+
// Wait 24 hours before sending reminder
105+
await sleep('24h'); // [!code highlight]
106+
107+
await sendReminder(message);
108+
109+
return 'reminder sent';
110+
}
111+
112+
async function sendReminder(message: string) {
113+
"use step";
114+
// Send reminder logic
115+
}
116+
```
117+
118+
<Callout type="info">
119+
The `sleep` function accepts duration strings like `'5s'`, `'10m'`, `'1h'`, `'24h'`, or milliseconds as a number. See the [sleep API reference](/docs/api-reference/workflow/sleep) for more details.
120+
</Callout>

packages/core/src/workflow.test.ts

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -947,6 +947,219 @@ describe('runWorkflow', () => {
947947
});
948948
});
949949

950+
describe('timeout functions', () => {
951+
it('should throw an error when calling setTimeout', async () => {
952+
const ops: Promise<any>[] = [];
953+
const workflowRun: WorkflowRun = {
954+
runId: 'test-run-123',
955+
workflowName: 'workflow',
956+
status: 'running',
957+
input: dehydrateWorkflowArguments([], ops),
958+
createdAt: new Date('2024-01-01T00:00:00.000Z'),
959+
updatedAt: new Date('2024-01-01T00:00:00.000Z'),
960+
startedAt: new Date('2024-01-01T00:00:00.000Z'),
961+
deploymentId: 'test-deployment',
962+
};
963+
964+
const events: Event[] = [];
965+
966+
await expect(
967+
runWorkflow(
968+
`async function workflow() {
969+
setTimeout(() => {}, 1000);
970+
return 'done';
971+
}${getWorkflowTransformCode('workflow')}`,
972+
workflowRun,
973+
events
974+
)
975+
).rejects.toThrow(
976+
'Timeout functions like "setTimeout" and "setInterval" are not supported in workflow functions'
977+
);
978+
});
979+
980+
it('should throw an error when calling setInterval', async () => {
981+
const ops: Promise<any>[] = [];
982+
const workflowRun: WorkflowRun = {
983+
runId: 'test-run-123',
984+
workflowName: 'workflow',
985+
status: 'running',
986+
input: dehydrateWorkflowArguments([], ops),
987+
createdAt: new Date('2024-01-01T00:00:00.000Z'),
988+
updatedAt: new Date('2024-01-01T00:00:00.000Z'),
989+
startedAt: new Date('2024-01-01T00:00:00.000Z'),
990+
deploymentId: 'test-deployment',
991+
};
992+
993+
const events: Event[] = [];
994+
995+
await expect(
996+
runWorkflow(
997+
`async function workflow() {
998+
setInterval(() => {}, 1000);
999+
return 'done';
1000+
}${getWorkflowTransformCode('workflow')}`,
1001+
workflowRun,
1002+
events
1003+
)
1004+
).rejects.toThrow(
1005+
'Timeout functions like "setTimeout" and "setInterval" are not supported in workflow functions'
1006+
);
1007+
});
1008+
1009+
it('should throw an error when calling clearTimeout', async () => {
1010+
const ops: Promise<any>[] = [];
1011+
const workflowRun: WorkflowRun = {
1012+
runId: 'test-run-123',
1013+
workflowName: 'workflow',
1014+
status: 'running',
1015+
input: dehydrateWorkflowArguments([], ops),
1016+
createdAt: new Date('2024-01-01T00:00:00.000Z'),
1017+
updatedAt: new Date('2024-01-01T00:00:00.000Z'),
1018+
startedAt: new Date('2024-01-01T00:00:00.000Z'),
1019+
deploymentId: 'test-deployment',
1020+
};
1021+
1022+
const events: Event[] = [];
1023+
1024+
await expect(
1025+
runWorkflow(
1026+
`async function workflow() {
1027+
clearTimeout(123);
1028+
return 'done';
1029+
}${getWorkflowTransformCode('workflow')}`,
1030+
workflowRun,
1031+
events
1032+
)
1033+
).rejects.toThrow(
1034+
'Timeout functions like "setTimeout" and "setInterval" are not supported in workflow functions'
1035+
);
1036+
});
1037+
1038+
it('should throw an error when calling clearInterval', async () => {
1039+
const ops: Promise<any>[] = [];
1040+
const workflowRun: WorkflowRun = {
1041+
runId: 'test-run-123',
1042+
workflowName: 'workflow',
1043+
status: 'running',
1044+
input: dehydrateWorkflowArguments([], ops),
1045+
createdAt: new Date('2024-01-01T00:00:00.000Z'),
1046+
updatedAt: new Date('2024-01-01T00:00:00.000Z'),
1047+
startedAt: new Date('2024-01-01T00:00:00.000Z'),
1048+
deploymentId: 'test-deployment',
1049+
};
1050+
1051+
const events: Event[] = [];
1052+
1053+
await expect(
1054+
runWorkflow(
1055+
`async function workflow() {
1056+
clearInterval(123);
1057+
return 'done';
1058+
}${getWorkflowTransformCode('workflow')}`,
1059+
workflowRun,
1060+
events
1061+
)
1062+
).rejects.toThrow(
1063+
'Timeout functions like "setTimeout" and "setInterval" are not supported in workflow functions'
1064+
);
1065+
});
1066+
1067+
it('should throw an error when calling setImmediate', async () => {
1068+
const ops: Promise<any>[] = [];
1069+
const workflowRun: WorkflowRun = {
1070+
runId: 'test-run-123',
1071+
workflowName: 'workflow',
1072+
status: 'running',
1073+
input: dehydrateWorkflowArguments([], ops),
1074+
createdAt: new Date('2024-01-01T00:00:00.000Z'),
1075+
updatedAt: new Date('2024-01-01T00:00:00.000Z'),
1076+
startedAt: new Date('2024-01-01T00:00:00.000Z'),
1077+
deploymentId: 'test-deployment',
1078+
};
1079+
1080+
const events: Event[] = [];
1081+
1082+
await expect(
1083+
runWorkflow(
1084+
`async function workflow() {
1085+
setImmediate(() => {});
1086+
return 'done';
1087+
}${getWorkflowTransformCode('workflow')}`,
1088+
workflowRun,
1089+
events
1090+
)
1091+
).rejects.toThrow(
1092+
'Timeout functions like "setTimeout" and "setInterval" are not supported in workflow functions'
1093+
);
1094+
});
1095+
1096+
it('should throw an error when calling clearImmediate', async () => {
1097+
const ops: Promise<any>[] = [];
1098+
const workflowRun: WorkflowRun = {
1099+
runId: 'test-run-123',
1100+
workflowName: 'workflow',
1101+
status: 'running',
1102+
input: dehydrateWorkflowArguments([], ops),
1103+
createdAt: new Date('2024-01-01T00:00:00.000Z'),
1104+
updatedAt: new Date('2024-01-01T00:00:00.000Z'),
1105+
startedAt: new Date('2024-01-01T00:00:00.000Z'),
1106+
deploymentId: 'test-deployment',
1107+
};
1108+
1109+
const events: Event[] = [];
1110+
1111+
await expect(
1112+
runWorkflow(
1113+
`async function workflow() {
1114+
clearImmediate(123);
1115+
return 'done';
1116+
}${getWorkflowTransformCode('workflow')}`,
1117+
workflowRun,
1118+
events
1119+
)
1120+
).rejects.toThrow(
1121+
'Timeout functions like "setTimeout" and "setInterval" are not supported in workflow functions'
1122+
);
1123+
});
1124+
1125+
it('should include documentation link in error message', async () => {
1126+
let error: Error | undefined;
1127+
try {
1128+
const ops: Promise<any>[] = [];
1129+
const workflowRun: WorkflowRun = {
1130+
runId: 'test-run-123',
1131+
workflowName: 'workflow',
1132+
status: 'running',
1133+
input: dehydrateWorkflowArguments([], ops),
1134+
createdAt: new Date('2024-01-01T00:00:00.000Z'),
1135+
updatedAt: new Date('2024-01-01T00:00:00.000Z'),
1136+
startedAt: new Date('2024-01-01T00:00:00.000Z'),
1137+
deploymentId: 'test-deployment',
1138+
};
1139+
1140+
const events: Event[] = [];
1141+
1142+
await runWorkflow(
1143+
`async function workflow() {
1144+
setTimeout(() => {}, 1000);
1145+
return 'done';
1146+
}${getWorkflowTransformCode('workflow')}`,
1147+
workflowRun,
1148+
events
1149+
);
1150+
} catch (err) {
1151+
error = err as Error;
1152+
}
1153+
assert(error);
1154+
expect(error.message).toContain(
1155+
'https://useworkflow.dev/err/timeout-in-workflow'
1156+
);
1157+
expect(error.message).toContain(
1158+
'Use the "sleep" function from "workflow"'
1159+
);
1160+
});
1161+
});
1162+
9501163
describe('hook', () => {
9511164
it('should throw `WorkflowSuspension` when a hook is awaiting without a "hook_received" event', async () => {
9521165
let error: Error | undefined;

0 commit comments

Comments
 (0)