Learn how the JavaScript event loop handles async code. Understand the call stack, task queue, microtasks, and why Promises always run before setTimeout().
How does JavaScript handle multiple things at once when it can only do one thing at a time? Why does this code print in a surprising order?
Even with a 0ms delay, Timeout prints last. The answer lies in the event loop. It’s JavaScript’s mechanism for handling asynchronous operations while remaining single-threaded.
What you’ll learn in this guide:
Why JavaScript needs an event loop (and what “single-threaded” really means)
How setTimeout REALLY works (spoiler: the delay is NOT guaranteed!)
The difference between tasks and microtasks (and why it matters)
Why Promise.then() runs before setTimeout(..., 0)
How to use setTimeout, setInterval, and requestAnimationFrame effectively
Common interview questions explained step-by-step
Prerequisites: This guide assumes familiarity with the call stack and Promises. If those concepts are new to you, read them first!
The event loop is JavaScript’s mechanism for executing code, handling events, and managing asynchronous operations. As defined in the WHATWG HTML Living Standard, it coordinates execution by checking callback queues when the call stack is empty, then pushing queued tasks to the stack for execution. This enables non-blocking behavior despite JavaScript being single-threaded.
Imagine a busy restaurant kitchen with a single chef who can only cook one dish at a time. Despite this limitation, the restaurant serves hundreds of customers because the kitchen has a clever system:
The chef (JavaScript) can only work on one dish (task) at a time. But kitchen timers (Web APIs) run independently! When a timer goes off, the dish goes to the “Order Up!” window (Task Queue). The kitchen manager (Event Loop) constantly checks: “Is the chef free? Here’s the next order!”VIP orders (Promises) always get priority. They jump ahead of regular orders in the queue.
TL;DR: JavaScript is single-threaded but achieves concurrency by delegating work to browser APIs, which run in the background. When they’re done, callbacks go into queues. The Event Loop moves callbacks from queues to the call stack when it’s empty.
JavaScript can only do one thing at a time. There’s one call stack, one thread of execution.
Copy
Ask AI
// JavaScript executes these ONE AT A TIME, in orderconsole.log('First'); // 1. This runsconsole.log('Second'); // 2. Then thisconsole.log('Third'); // 3. Then this
Imagine if every operation blocked the entire program. Consider the Fetch API:
Copy
Ask AI
// If fetch() was synchronous (blocking)...const data = fetch('https://api.example.com/data'); // Takes 2 secondsconsole.log(data);// NOTHING else can happen for 2 seconds!// - No clicking buttons// - No scrolling// - No animations// - Complete UI freeze!
A 30-second API call would freeze your entire webpage for 30 seconds. Users would think the browser crashed! According to Google’s Core Web Vitals research, any interaction that takes longer than 200 milliseconds to respond is perceived as sluggish by users.
JavaScript solves this by delegating long-running tasks to the browser (or Node.js), which handles them in the background. Functions like setTimeout() don’t block:
The Call Stack is where JavaScript keeps track of what function is currently running. It’s a LIFO (Last In, First Out) structure, like a stack of plates.
Copy
Ask AI
function multiply(a, b) { return a * b;}function square(n) { return multiply(n, n);}function printSquare(n) { const result = square(n); console.log(result);}printSquare(4);
The Heap is a large, mostly unstructured region of memory where objects, arrays, and functions are stored. When you create an object, it lives in the heap.
Copy
Ask AI
const user = { name: 'Alice' }; // Object stored in heapconst numbers = [1, 2, 3]; // Array stored in heap
Web APIs (Browser) / C++ APIs (Node.js)
These are NOT part of JavaScript itself! They’re provided by the environment:Browser APIs:
Microtasks ALWAYS run before the next task! The entire microtask queue is drained before moving to the task queue.
Event Loop
The Event Loop is the orchestrator. Its job is simple but crucial:
Copy
Ask AI
FOREVER: 1. Execute all code in the Call Stack until empty 2. Execute ALL microtasks (until microtask queue is empty) 3. Render if needed (update the UI) 4. Take ONE task from the task queue 5. Go to step 1
The key insight: Microtasks can starve the task queue! If microtasks keep adding more microtasks, tasks (and rendering) never get a chance to run.
console.log('1') → prints “1”setTimeout → registers callback in Web APIs → callback goes to Task QueuePromise.resolve().then() → callback goes to Microtask Queueconsole.log('4') → prints “4”
Output:Start, End, Promise 1, Promise 2, TimeoutEven though the second promise is created AFTER setTimeout was registered, it still runs first because the entire microtask queue must be drained before any task runs!
What about requestAnimationFrame? rAF is NOT a task. It runs during the rendering phase, after microtasks but before the browser paints. It’s covered in detail in the Timers section.
// Pseudocode for the Event Loop (per HTML specification)while (true) { // 1. Process ONE task from the task queue (if available) if (taskQueue.hasItems()) { const task = taskQueue.dequeue(); execute(task); } // 2. Process ALL microtasks (until queue is empty) while (microtaskQueue.hasItems()) { const microtask = microtaskQueue.dequeue(); execute(microtask); // New microtasks added during execution are also processed! } // 3. Render if needed (browser decides, typically ~60fps) if (shouldRender()) { // 3a. Run requestAnimationFrame callbacks runAnimationFrameCallbacks(); // 3b. Perform style calculation, layout, and paint render(); } // 4. Repeat (go back to step 1)}
Microtask Starvation: If microtasks keep adding more microtasks, the task queue (and rendering!) will never get a chance to run:
Copy
Ask AI
// DON'T DO THIS - infinite microtask loop!function forever() { Promise.resolve().then(forever);}forever(); // Browser freezes!
// Syntaxconst timerId = setTimeout(callback, delay, ...args);// Cancel before it runsclearTimeout(timerId);
Basic usage:
Copy
Ask AI
// Run after 2 secondssetTimeout(() => { console.log('Hello after 2 seconds!');}, 2000);// Pass arguments to the callbacksetTimeout((name, greeting) => { console.log(`${greeting}, ${name}!`);}, 1000, 'Alice', 'Hello');// Output after 1s: "Hello, Alice!"
Canceling a timeout:
Copy
Ask AI
const timerId = setTimeout(() => { console.log('This will NOT run');}, 5000);// Cancel it before it firesclearTimeout(timerId);
setInterval doesn’t account for callback execution time:
Copy
Ask AI
// Problem: If callback takes 300ms, and interval is 1000ms,// actual time between START of callbacks is 1000ms,// but time between END of one and START of next is only 700mssetInterval(() => { // This takes 300ms to execute heavyComputation();}, 1000);
Copy
Ask AI
Time: 0ms 1000ms 2000ms 3000ms │ │ │ │setInterval│───────│────────│────────│ │ 300ms │ 300ms │ 300ms │ │callback│callback│callback│ │ │ │ │The 1000ms is between STARTS, not between END and START
// Nested setTimeout guarantees delay BETWEEN executionsfunction preciseInterval(callback, delay) { function tick() { callback(); setTimeout(tick, delay); // Schedule next AFTER current completes } setTimeout(tick, delay);}// Now there's exactly `delay` ms between the END of one// callback and the START of the next
function animate(timestamp) { // timestamp = time since page load in ms // Update animation state element.style.left = (timestamp / 10) + 'px'; // Request next frame requestAnimationFrame(animate);}// Start the animationrequestAnimationFrame(animate);
One Event Loop Iteration:┌─────────────────────────────────────────────────────────────────┐│ 1. Run task from Task Queue │├─────────────────────────────────────────────────────────────────┤│ 2. Run ALL microtasks │├─────────────────────────────────────────────────────────────────┤│ 3. If time to render: ││ a. Run requestAnimationFrame callbacks ← HERE! ││ b. Render/paint the screen │├─────────────────────────────────────────────────────────────────┤│ 4. If idle time remains before next frame: ││ Run requestIdleCallback callbacks (non-essential work) │└─────────────────────────────────────────────────────────────────┘
const start = Date.now();setTimeout(() => { console.log(`Elapsed: ${Date.now() - start}ms`);}, 1000);// Simulate heavy computationlet sum = 0;for (let i = 0; i < 1000000000; i++) { sum += i;}console.log('Heavy work done');
Answer
Problem: The timeout will NOT fire after 1000ms!The heavy for loop blocks the call stack. Even though the timer finishes after 1000ms, the callback cannot run until the call stack is empty.Typical output:
Copy
Ask AI
Heavy work doneElapsed: 3245ms // Much longer than 1000ms!
Lesson: Never do heavy synchronous work on the main thread. Use:
Wrong! The delay is a MINIMUM wait time, not a guarantee.If the call stack is busy or the Task Queue has items ahead, the actual delay will be longer.
Copy
Ask AI
setTimeout(() => console.log('A'), 100);setTimeout(() => console.log('B'), 100);// Heavy work takes 500msfor (let i = 0; i < 1e9; i++) {}// Both A and B fire at ~500ms, not 100ms
Misconception 3: 'JavaScript is asynchronous'
Partially wrong! JavaScript itself is single-threaded and synchronous.The asynchronous behavior comes from:
The runtime environment (browser/Node.js)
Web APIs that run in separate threads
The Event Loop that coordinates callbacks
JavaScript code runs synchronously, one line at a time. The magic is that it can delegate work to the environment.
Misconception 4: 'The Event Loop is part of JavaScript'
Wrong! The Event Loop is NOT defined in the ECMAScript specification.It’s defined in the HTML specification (for browsers) and implemented by the runtime environment. Different environments (browsers, Node.js, Deno) have different implementations.
Misconception 5: 'setInterval is accurate'
Wrong! setInterval can drift, skip callbacks, or have inconsistent timing.
If a callback takes longer than the interval, callbacks queue up
Browsers may throttle timers in background tabs
Timer precision is limited (especially on mobile)
For precise timing, use nested setTimeout or requestAnimationFrame.
When synchronous code runs for a long time, EVERYTHING stops:
Copy
Ask AI
// This freezes the entire page!button.addEventListener('click', () => { // Heavy synchronous work for (let i = 0; i < 10000000000; i++) { // ... computation }});
function processInChunks(items, process, chunkSize = 100) { let index = 0; function doChunk() { const end = Math.min(index + chunkSize, items.length); for (; index < end; index++) { process(items[index]); } if (index < items.length) { setTimeout(doChunk, 0); // Yield to event loop } } doChunk();}// Now UI stays responsive between chunksprocessInChunks(hugeArray, item => compute(item));
Answer: The Event Loop’s job is to monitor the call stack and the callback queues. When the call stack is empty, it takes the first callback from the microtask queue (if any), or the task queue, and pushes it onto the call stack for execution.It enables JavaScript to be non-blocking despite being single-threaded.
Question 2: Why do Promises run before setTimeout?
Answer: Promise callbacks go to the Microtask Queue, while setTimeout callbacks go to the Task Queue (macrotask queue).The Event Loop always drains the entire microtask queue before taking the next task from the task queue. So Promise callbacks always have priority.
Answer: If the fetch takes longer than 1 second, multiple requests will be in flight simultaneously, potentially causing race conditions and overwhelming the server.Better approach:
Copy
Ask AI
async function poll() { const response = await fetch('/api/data'); const data = await response.json(); updateUI(data); setTimeout(poll, 1000); // Schedule next AFTER completion}poll();
Question 6: How can you yield to the Event Loop in a long-running task?
Answer: Several approaches:
Copy
Ask AI
// 1. setTimeout (schedules a task)await new Promise(resolve => setTimeout(resolve, 0));// 2. queueMicrotask (schedules a microtask)await new Promise(resolve => queueMicrotask(resolve));// 3. requestAnimationFrame (syncs with rendering)await new Promise(resolve => requestAnimationFrame(resolve));// 4. requestIdleCallback (runs during idle time)await new Promise(resolve => requestIdleCallback(resolve));
Each has different timing and use cases. setTimeout is most common for yielding.
The event loop is JavaScript’s mechanism for handling asynchronous operations while remaining single-threaded. As defined in the WHATWG HTML Living Standard, it continuously checks whether the call stack is empty and then dequeues tasks from the task queue or microtask queue for execution. This is what allows non-blocking I/O in both browsers and Node.js.
What is the difference between microtasks and macrotasks?
Microtasks (Promise callbacks, queueMicrotask, MutationObserver) run after the current task completes but before the next macrotask. Macrotasks (setTimeout, setInterval, I/O) are queued in the task queue and processed one per event loop iteration. The key rule: the entire microtask queue is drained before the next macrotask runs.
Why does Promise.then() run before setTimeout(0)?
Promise callbacks are scheduled as microtasks, while setTimeout callbacks are scheduled as macrotasks. According to the HTML specification’s event loop processing model, all microtasks are processed before the event loop picks up the next macrotask. This is why Promise.then() always executes before setTimeout(..., 0) even though both are asynchronous.
How does JavaScript handle async operations if it is single-threaded?
JavaScript delegates long-running operations (network requests, timers, file I/O) to the browser’s Web APIs or Node.js’s libuv thread pool, which run on separate threads. When those operations complete, their callbacks are placed into the appropriate queue. The event loop then picks them up when the call stack is empty. This gives the illusion of parallelism while keeping JavaScript execution single-threaded.
What is the difference between concurrency and parallelism in JavaScript?
Concurrency means managing multiple tasks by interleaving them on a single thread, which is what the event loop provides. Parallelism means executing multiple tasks simultaneously on different threads, which requires Web Workers. According to MDN, async/await and Promises give you concurrency, while Web Workers give you true parallelism.