Skip to content
Advanced11 min study

Event loop

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

Question progress0 / 10 completed
Start the lesson
Event loop visual explanation

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.

Think of it like this: a single chef in a kitchen (the call stack) who can only do one thing at a time, a waiter dropping off new order tickets as they arrive (the task queue), and a strict rule that the chef must finish plating every dish already in progress — including anything that spins off from it — before even glancing at the next new ticket (microtasks always drain before the next task).

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.

JavaScript
console.log('A');setTimeout(() => console.log('B'), 0);Promise.resolve().then(() => console.log('C'));console.log('D');// Output: A D C B
Synchronous code finishes first (A, D). The microtask queue drains next (C). Only then does the timer's macrotask run (B).

04Rendering

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

How to say it out loud: "JavaScript runs on a single thread, so it can only do one thing at a time — synchronous code always runs to completion before anything else gets a turn. Asynchronous work like timers and promises doesn't block that thread; instead, when it's ready, it gets queued up. Two queues matter here: the microtask queue, used by promises, which is always fully drained before anything else happens, and the macrotask queue, used by timers and events, which only gets one task processed before the engine checks microtasks again. So for a resolved promise versus a zero-millisecond timeout, the promise callback always runs first, because the microtask queue empties before the next macrotask is even picked up."

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.
JavaScript
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 task

The 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.

JavaScript
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)
Open question page →
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.

JavaScript
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));  }}
Open question page →
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?

  1. Recognize that promise continuations are microtasks.
  2. Check whether every microtask schedules another before the queue empties.
  3. Measure long tasks and input delay in the performance panel.
  4. 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.

Verify and go deeper