Event loop
Trace tasks, microtasks, rendering opportunities, and async execution.

WHAT YOU WILL BE ABLE TO DO
Learning outcomes
- Explain event loop in plain language.
- Connect the behavior to the underlying browser or framework model.
- Implement the core pattern and reason through edge cases.
- Answer common follow-ups without relying on memorized phrases.
01Execution model
JavaScript runs on a single thread with one call stack. Synchronous code always runs to completion; the engine never interrupts a running function to start another one.
02Task queues
Macrotasks — timers, I/O callbacks, UI events, message-channel callbacks — are queued and processed one at a time. After a macrotask finishes, the engine checks the microtask queue before picking up the next macrotask.
03Microtasks
Promise callbacks, queueMicrotask, and MutationObserver callbacks are microtasks. The microtask queue is drained completely — including any microtasks scheduled by earlier microtasks — before the next macrotask or paint.
console.log('A');setTimeout(() => console.log('B'), 0);Promise.resolve().then(() => console.log('C'));console.log('D');// Output: A D C B04Rendering
The browser can paint after the microtask queue drains and before the next macrotask, if a frame is due. requestAnimationFrame callbacks run just before that paint, which is why they're preferred over setTimeout(fn, 0) for visual updates.
05Check understanding
Does await pause the whole thread?
No. await suspends only the async function; the thread is free to run other tasks and microtasks while the awaited promise settles.
DDConcept deep dives
Deep dive 1
JavaScript runs inside a host
The ECMAScript engine executes jobs and function calls, while the browser supplies timers, networking, events, rendering, and task queues. Calling setTimeout registers work with the host and returns; it does not place a sleeping function on the call stack. When the timer becomes eligible, its callback still waits until the event loop can select its task and invoke it on a fresh stack.
- Run-to-completion means another task does not interrupt the current JavaScript task.
- A delay is a minimum eligibility time, not a guaranteed start time.
- Browser and Node.js scheduling details differ, so state the environment in queue-order questions.
Deep dive 2
Microtasks run before the browser moves on
Promise reactions and queueMicrotask callbacks enter the microtask mechanism. After the current task finishes, the host drains microtasks, including new microtasks added during that drain, before selecting another task. This gives promise chains predictable ordering but creates starvation risk: an endless microtask chain prevents timers, input, and rendering from progressing.
- Synchronous code finishes before any promise reaction runs.
- A resolved promise still invokes its handler asynchronously.
- Splitting heavy work across promises does not yield a rendering opportunity.
console.log('task start');setTimeout(() => console.log('timer task'), 0);Promise.resolve().then(() => console.log('microtask'));console.log('task end');
// task start, task end, microtask, timer taskThe script is one task. Its synchronous statements finish, microtasks drain, and only then can the timer task be selected.
Deep dive 3
Responsiveness requires bounded main-thread work
Rendering and input handling compete with application JavaScript for the main thread. Work that runs longer than a frame budget delays visual feedback, while a long task can delay an interaction far more seriously. Reduce the work, move pure CPU computation to a Worker, or process bounded chunks that genuinely yield to later tasks. Then measure the complete interaction rather than only the handler function.
- Prioritize immediate feedback before nonessential computation.
- Cancellation prevents obsolete chunks or worker results from wasting resources.
- A task scheduler changes when work runs; it does not reduce algorithmic complexity.
QAInterview questions and model answers
Attempt each answer aloud before opening it. The model answer shows the depth and precision expected in an interview; it is not a script to memorize.
Intermediate · Conceptual · 1 min · Question 1What does the JavaScript event loop coordinate?Open model answer
Model answer
It coordinates execution between the JavaScript call stack and host-managed queues. A task runs to completion; after it finishes, the runtime drains eligible microtasks before the browser gets its next rendering opportunity and selects another task.
Open question page →Intermediate · Conceptual · 1 min · Question 2What is the difference between a task and a microtask?Open model answer
Model answer
Tasks include events, timers, and initial script execution. Promise reactions and queueMicrotask callbacks are microtasks. After a task finishes, the microtask queue is drained before the next task; continually adding microtasks can therefore delay rendering and input.
Open question page →Intermediate · Conceptual · 1 min · Question 3Why does setTimeout(fn, 0) not run immediately?Open model answer
Model answer
It schedules a task after the minimum delay has elapsed. The callback still waits for the current task, all pending microtasks, and earlier eligible tasks. Browsers may also clamp nested or background timers.
Open question page →Intermediate · Conceptual · 1 min · Question 4Where does rendering occur relative to JavaScript?Open model answer
Model answer
The browser may render between tasks when the rendering opportunity is due, after microtasks have drained. Long tasks and microtask starvation prevent that opportunity, which is why splitting CPU work only with resolved promises may not restore responsiveness.
Open question page →Intermediate · Coding · 1 min · Question 5What order does console.log produce for synchronous code, a resolved promise, and a timer?Open model answer
Model answer
Synchronous logs run first. The resolved promise reaction runs as a microtask after the current script task. The timer runs as a later task, assuming no earlier work. The useful explanation is queue semantics, not memorizing one puzzle.
console.log('A');setTimeout(() => console.log('B')); // macrotaskPromise.resolve().then(() => console.log('C')); // microtaskconsole.log('D');
// Output: A D C B// sync (A, D) -> drain microtasks (C) -> next task (B)Advanced · Coding · 1 min · Question 6How would you yield during a large browser computation?Open model answer
Model answer
Break work into bounded chunks and schedule continuation through a task-producing mechanism or an appropriate scheduling API so rendering and input can run between chunks. A chain of queueMicrotask calls does not yield to rendering because microtasks drain before the next rendering opportunity.
async function processInChunks(items, work, chunkSize = 500) { for (let i = 0; i < items.length; i += chunkSize) { for (let j = i; j < i + chunkSize && j < items.length; j++) work(items[j]); // hand the thread back so the browser can paint / handle input await new Promise((r) => setTimeout(r)); }}Advanced · Conceptual · 1 min · Question 7Where does requestAnimationFrame run?Open model answer
Model answer
The browser invokes animation-frame callbacks before a rendering opportunity when the document is eligible. The callback still runs on the main thread and can delay the frame if expensive.
Open question page →Beginner · Conceptual · 1 min · Question 8Can a microtask interrupt synchronous JavaScript?Open model answer
Model answer
No. The current job or task runs to completion first. Microtasks drain at defined checkpoints after the stack unwinds, not in the middle of an ordinary statement sequence.
Open question page →Advanced · Conceptual · 1 min · Question 9Why can alert change scheduling demonstrations?Open model answer
Model answer
Blocking dialogs pause or alter normal browser processing and differ across environments. Queue-order experiments should avoid them and distinguish specified ordering from host-specific rendering behavior.
Open question page →Intermediate · Conceptual · 1 min · Question 10What is a long task?Open model answer
Model answer
In web performance tooling, a main-thread task exceeding 50 milliseconds is reported as a long task. The blocking portion beyond 50 milliseconds contributes to total blocking time.
Open question page →SCScenario questions
Scenario 1
Typing becomes unresponsive while filtering a large dataset, even though each chunk is scheduled with Promise.resolve().then(...). Why?
- Recognize that promise continuations are microtasks.
- Check whether every microtask schedules another before the queue empties.
- Measure long tasks and input delay in the performance panel.
- Move computation to a worker or yield with task-sized chunks.
Reveal worked answer
The promise chain can starve the browser because microtasks are drained before rendering or another task. I would use a Worker for substantial pure computation. If the work must stay on the main thread, I would process a bounded chunk, schedule the next chunk as a later task, support cancellation, and verify responsiveness with performance traces.