A timer asks the browser to wait before making a callback eligible. It does not reserve the thread at a deadline. Press play and follow one TypeScript script: it prints start, schedules a 10 ms timer, queues A and B, performs 80 ms of synchronous work, and prints end. The busy helper repeatedly reads performance.now() until the requested interval has passed; it is deliberately blocking work used to expose the scheduling mechanism.
The currently running task keeps the thread until it returns. The film's illustrative clock reaches the timer threshold at 10 ms while busy(80) is still executing. Eligibility changes, but execution does not interrupt the script. In this example the browser can only run the callback after the long task and the following microtask checkpoint have finished.
At that checkpoint, A runs first and queues C. C joins behind the already queued B. The checkpoint keeps taking the oldest microtask until its queue is empty, including microtasks appended by other microtasks. The resulting console order is start, end, A, B, C, timer. The TypeScript fixture executes in Chromium and WebKit, three times each, and the film's finite pure model reproduces that order.
Splitting the same work into four 20 ms chunks is not sufficient by itself. With await Promise.resolve(), each async continuation is a microtask, so the checkpoint keeps running the remaining chunks before the timer gets a turn. With await new Promise<void>((resolve) => setTimeout(resolve, 0)), the current continuation suspends until a later timer task resolves its promise. That resolution queues the async continuation as a microtask. Other tasks now have an opportunity to run between chunks.
The local captures make that opportunity concrete. The observed 10 ms timer ran at about 80 ms after the long task or promise yields. With timer task yields it ran around 40 ms in Chromium and 20 ms in WebKit. Neither value is a browser guarantee. The controls select a stored three-run trace and seek to its film chapter; the engine control changes the measured range and console order beneath the player. These are genuine observations of the same TypeScript source, with capture version and source hashes kept beside the fixture.
For work that affects responsiveness, keep requested delay, eligibility and actual callback execution separate. Yield to another task when appropriate, or move substantial computation to a worker. A promise continuation alone does not give timer tasks a turn. This explainer models one agent, a finite microtask queue and one timer task source. It does not model rendering opportunities, input prioritisation, cross-agent parallelism, background throttling or exact browser timer insertion times. In a real application, profile the work and measure responsiveness under representative load.
The maths
- Delay and execution are separate
The timer delay d is a minimum waiting condition. The film models eligibility at the threshold; the browser can make the callback runnable later and must still obtain a turn to execute it.
- The worked example
The synchronous busy loop holds this agent for 80 ms. Its queued microtasks run before the timer callback. Small logging and scheduling costs make measured timing approximate.
- Chunking does not choose a queue
Awaiting a fulfilled promise splits the source into continuations but keeps them in the microtask checkpoint. Awaiting a timer gives another task an opportunity; its resolver then schedules the async continuation as a microtask.
Sources and model assumptions
Follow the original mechanism behind this explainer. The interactive examples identify their toy data and simplifying assumptions above.