cloudflare / cloudflare/workerd

Support setTimeout with delay=0 called from global scope

Open
#389 15 comments 0 reactions 0 assignees View on GitHub
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.