cloudflare / cloudflare/workerd
Support setTimeout with delay=0 called from global scope
- Dominant language
- C++
- Stars
- 8.7k
- Forks
- 739
- Avg merge
- 2d 20h
- Merged PRs (30d)
- 174
Description
Calling `setTimeout` from the global scope (outside of a request context) is currently not supported by workerd. If a code attempts to call it:
```
setTimeout(() => {
console.log("we'll never get here :-/");
}, 0);
```
an exception is thrown by the runtime:
```
Uncaught Error: Some functionality, such as asynchronous I/O, timeouts, and generating random values, can only be performed while handling a request.
```
Existing code and libraries currently rely on calling setTimeout with delayMs set to 0 (or undefined) from global scope to schedule tasks or emulate microtask scheduling. A good example is esbuild-wasm (the browser flavor): https://github.com/evanw/esbuild/blob/d8b028fc62bb6a4115a841b9ba9d0c747a0d54d1/lib/npm/browser.ts#L90
While we don't want code running in our server runtime to schedule tasks during isolate boostrap with a delay, the current behavior causes interop and code portability issues. A reasonable compromise would be to allow `setInterval` to be invoked from a global scope as long as the delay is undefined or is set to 0ms. This could under the hood be implemented by scheduling the task as a microtask via the `queueMicrotask` API — the difference between a task and microtask should not be observable to the developer in this limited server-only scenario.
A simple polyfill implementation of this change that can be used by anyone blocked by this issue looks like this:
```js
const originalSetTimeout = globalThis.setTimeout;
const originalClearTimeout = globalThis.clearTimeout;
// hack to minimize possibility of possible timer ID collisions
var setTimeoutCounter = Number.MAX_SAFE_INTEGER - 1_000_000;
var pendingTimers = new Set();
globalThis.setTimeout = function setTimeout(callback, delay, ...args) {
if (delay > 0) {
return originalSetTimeout(callback, delay, ...args);
}
var timerId = setTimeoutCounter++;
pendingTimers.add(timerId);
queueMicrotask(() => {
if (pendingTimers.has(timerId)) {
pendingTimers.delete(timerId);
callback();
}
});
return timerId;
};
globalThis.clearTimeout = function clearTimeout(timerId) {
if (pendingTimers.has(timerId)) {
pendingTimers.delete(timerId);
}
return originalClearTimeout(timerId);
}
```
Contributor guide
Assessment
This issue has not been assessed yet.