Task vs Microtask Ordering
In the browser event loop, a task runs to completion before the event loop proceeds to another task. After the task's JavaScript finishes, pending microtasks are generally drained before the next task is selected.
Tasks include work such as timer callbacks, user events, and other event-loop callbacks. JavaScript executes the currently running task synchronously until its call stack becomes empty; it is not interrupted halfway through by another task.
Microtasks include promise reaction callbacks and `queueMicrotask()` callbacks. When JavaScript finishes the current task, the microtask queue is processed, with newly queued microtasks continuing to be processed during that checkpoint.
This ordering matters when code mixes synchronous statements, promises, and timers. A promise callback scheduled during a task normally runs before a timer callback scheduled during that same task, even if the timer was registered first.
After the microtask queue is drained, the browser can proceed with other event-loop work, including selecting another task and performing rendering at appropriate points. Exact rendering opportunities depend on the browser and event-loop context.
Key points
- Synchronous JavaScript in a task runs to completion.
- Promise callbacks and `queueMicrotask()` use the microtask queue.
- Microtasks are generally drained after a task and before the next task.
- A timer callback is a task, so it normally runs after pending microtasks.
- Microtasks can enqueue more microtasks, which are processed during the same checkpoint.
Interview questions
What is the difference between a task and a microtask?
A task is a unit of event-loop work such as a timer callback or an event callback. A microtask is smaller queued work such as a promise reaction or `queueMicrotask()` callback. After the current task finishes, the runtime generally drains pending microtasks before moving to another task.
What does this print, and why: `console.log(1); Promise.resolve().then(() => console.log(2)); setTimeout(() => console.log(3), 0); console.log(4);`?
It prints `1`, then `4`, then `2`, then `3`. The first and last `console.log` calls are synchronous and execute in the current task. The promise reaction is a microtask, so it runs after the current task's synchronous code. The timer callback is a later task, so it runs after the microtask checkpoint.
Why can a promise callback run before a zero-delay timer?
A zero-delay timer does not mean its callback executes immediately. It schedules a timer task that becomes eligible according to the event loop's rules. A promise reaction is a microtask, and pending microtasks are generally processed after the current task before the event loop takes another task. Therefore, a promise callback queued during the current task normally runs before that timer callback.
Can microtasks cause a responsiveness problem?
Yes. If code continually adds more microtasks, the microtask checkpoint can take a long time to finish. The browser generally does not move on to another task while that microtask work is still being drained. Excessive microtask work can therefore delay timers, user-event handling, and opportunities for rendering.