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.
Transcript
The narration, chapter by chapter. A timestamp opens the film at that moment.
- 0:00Delay is a minimum
- A ten-millisecond timer is a request to wait at least ten milliseconds, and this example shows why its callback still cannot run when that wait ends.
- 0:10Schedule the work
- The script prints start, schedules the timer, queues two microtasks, and then holds the thread for eighty milliseconds before printing end, with every line visible in TypeScript.
- 0:20Run to completion
- When our clock reaches ten, the timer becomes eligible in this model, but the current task keeps the thread until eighty, so becoming eligible never interrupts synchronous code.
- 0:31The checkpoint
- After the script returns, the microtask checkpoint runs A first, and the timer keeps waiting because the browser drains this queue before selecting another task.
- 0:41New microtasks join
- A now appends C behind B, so the same checkpoint runs B and then C, giving start, end, A, B, C, timer in both browsers we tested.
- 0:51A promise still waits
- Splitting the work into four chunks changes nothing if each continuation awaits an already fulfilled promise, because those continuations are microtasks and our timer still waits roughly eighty milliseconds.
- 1:02A chance to run
- Task-level yields create a chance to run between chunks: our two browsers ran the timer around forty and twenty milliseconds, showing why yielding improves responsiveness without promising an exact start time.
- 1:14Three separate moments
- Keep timer delay, queue eligibility, and callback execution separate, and for expensive work choose task-level yields or a worker, because a promise continuation alone does not give timer tasks a turn.
Also available as captions (WebVTT), the video file and a Markdown copy of this page.
Sources and model assumptions
Follow the original mechanism behind this explainer. The interactive examples identify their toy data and simplifying assumptions above.
More visualisations
Embed this film
Paste this into a post, a course page or a newsletter. The film plays in place, with the same controls as here, and credits the source.