JavaScript / Advanced: scheduling and trustworthy interfaces / Lesson 13
Event loops, tasks, and microtasks
Predict observable scheduling without inventing a single universal timer queue. Separate ECMAScript jobs from browser tasks, microtask checkpoints, rendering opportunities, and Node host behavior.
On this page
Lesson goals
- Trace synchronous code and promise continuations.
- Distinguish tasks from microtasks and rendering.
- Recognize starvation and host-specific ordering limits.
The language and host cooperate
ECMAScript describes jobs and execution rules; the browser host integrates them with event loops, task sources, and microtask checkpoints. A timer callback is a task in the relevant browser model, whereas a promise reaction and queueMicrotask callback use microtask scheduling. There is no useful guarantee that every kind of browser activity belongs to one globally FIFO task queue. Node has its own event-loop phases and extra scheduling facilities. Learn the guarantees required by the example rather than importing a diagram from one host and treating it as the language specification for every environment.
Trace one turn before the timer
The script first runs its synchronous statements. The fulfilled promise schedules a reaction; the timer schedules later callback eligibility rather than interrupting the active script. After the current synchronous work, the microtask reaction runs before this timer callback in the controlled example. A zero timer delay is not an exact execution deadline and does not promise a paint first. The result is a sequence of observations under stated conditions, not proof about the relative order of unrelated network events, rendering, or Node-specific nextTick callbacks that are absent from the snippet.
console.log("start");
setTimeout(() => console.log("timer"), 0);
Promise.resolve().then(() => console.log("promise"));
console.log("end");
start
end
promise
timer
The timer cannot interrupt the current script. This limited order is validated in both Node and a local browser, without claiming identical host event loops.
Microtasks can enqueue more microtasks
At a microtask checkpoint, queued microtasks are processed until the queue is empty, including newly queued work. This explains why a nested queueMicrotask can run before a timer even when the timer was registered earlier. It also creates a starvation risk: endlessly adding microtasks can prevent progress to other tasks and rendering opportunities. Promise-based repetition is not a universal yield-to-paint technique. A long chain may be asynchronous from the function callerโs perspective while still making the interface unresponsive. Bound work and choose a host-appropriate scheduling boundary when actual user interaction needs time.
Follow enqueue order explicitly
The example adds A and B during the script. When A runs, it adds C after the already queued B. The timer remains later than the resulting finite checkpoint. Keep a written queue trace while learning: identify the currently running callback, then note what it appends. Do not infer order from indentation or from whether a callback looks expensive. If one operation genuinely depends on another, express the dependency with returned promises or explicit state rather than relying on an incidental timer race that changes with host load or surrounding code.
queueMicrotask(() => {
console.log("A");
queueMicrotask(() => console.log("C"));
});
queueMicrotask(() => console.log("B"));
setTimeout(() => console.log("T"), 0);
A
B
C
T
C is appended behind B. The example deliberately stops adding microtasks, allowing the timer to run.
Scheduling order is not a performance budget
Even a correctly ordered callback can take too long. Measure the duration and amount of synchronous work, then reduce it or move suitable computation across a worker boundary. Browser rendering may occur at opportunities chosen by the host; do not assert one paint per timer or one frame per promise. requestAnimationFrame coordinates work with a rendering opportunity, but expensive work inside it can still miss a frame. Tests should observe meaningful state and use controlled scheduling for races rather than asserting precise wall-clock milliseconds that vary across machines and background tabs.
Common errors and fixes
A zero timer is treated as immediate
- Why it happens
- Eligibility delay is mistaken for a guaranteed deadline.
- How to fix it
- Express dependencies explicitly and observe completion state.
- Prevent it next time
- Avoid exact timing assertions for ordinary scheduling.
Repeated await is used to guarantee a paint
- Why it happens
- Microtask continuation is confused with returning to other tasks.
- How to fix it
- Bound work and use an appropriate browser scheduling boundary.
- Prevent it next time
- Measure responsiveness rather than counting await keywords.
A browser queue diagram is applied directly to Node
- Why it happens
- Host-specific phases and APIs are ignored.
- How to fix it
- State the host and restrict ordering claims to guaranteed cases.
- Prevent it next time
- Keep Node-specific scheduling examples in the Node course.
Check your understanding
1. Why does C follow B in the nested example?
Show worked solution
B was already queued when A began. A appends C, so the finite checkpoint processes B before C. The timer follows after those microtasks here. This is queue reasoning, not a promise that unrelated task sources are globally ordered by registration time.
2. A page freezes while an async loop repeatedly awaits resolved promises. Explain.
Show worked solution
Each continuation can enqueue more microtask work before the checkpoint empties, delaying other tasks and rendering. Reduce or bound the work, schedule suitable chunks across real task boundaries, or use a worker for appropriate independent computation. Verify user-visible progress under representative input sizes.
Key takeaways
- Task and microtask are distinct host-integrated scheduling concepts.
- Finite queue traces explain order; measured budgets explain responsiveness.