FrontendJavaScriptAsyncPromiseES6Frontend

JavaScript Event Loop: Understand Async Execution and the Call Stack

The event loop is the engine behind all JavaScript asynchronous behavior. Understanding it deeply will finally explain why setTimeout runs last and why Promises behave differently from callbacks.

Abdur Razzak

Abdur Razzak

Full-Stack Web Developer

May 26, 2026 9 min read

JavaScript is a single-threaded language, meaning it can only execute one piece of code at a time on a single call stack. Yet it handles asynchronous operations like network requests, timers, and user events without blocking the main thread. This apparent contradiction is resolved by the event loop, a mechanism built into JavaScript runtime environments that coordinates the execution of synchronous code, asynchronous callbacks, and microtasks. The event loop is not part of the JavaScript language specification but is a feature of the runtime environments where JavaScript runs, such as the V8 engine in browsers and Node.js. Understanding how the event loop works explains otherwise mysterious behaviors like why a setTimeout with zero delay does not execute immediately, why Promise callbacks run before setTimeout callbacks, and why certain patterns cause the browser to become unresponsive during heavy computation.

The Call Stack: How Synchronous Code Executes

The call stack is a data structure that tracks which functions are currently executing. When a function is called, the runtime pushes a stack frame containing that function's execution context onto the top of the stack. When the function returns, its frame is popped off the stack and execution resumes in the function below it. Synchronous code runs sequentially and predictably through this stack. When the call stack is empty, the JavaScript engine is idle and the event loop can begin processing pending callbacks. If a function calls itself recursively without a base case, or if a chain of synchronous function calls becomes extremely deep, the stack exceeds its size limit and throws a maximum call stack size exceeded error, commonly known as a stack overflow. Long synchronous operations like heavy computation or large data transformations block the call stack entirely, preventing the browser from processing user events or rendering updates until the operation completes.

The Web API Layer and the Callback Queue

The browser and Node.js runtime environments provide Web APIs that execute certain operations outside the main JavaScript thread. When you call setTimeout, addEventListener, or fetch, the actual work is handled by the browser Web API environment rather than the JavaScript engine itself. The timer is tracked by the browser timer mechanism, the network request is handled by the browser networking layer, and the event listener is registered with the browser event system. When one of these operations completes, such as when a timer expires or a network response arrives, the associated callback function is placed in the callback queue, also called the macrotask queue. The event loop continuously monitors both the call stack and the callback queue. When the call stack is empty, the event loop takes the first callback from the callback queue and pushes it onto the call stack for execution.

The Microtask Queue: Higher Priority than Callbacks

The microtask queue is a separate, higher-priority queue that processes certain types of asynchronous completions. Promise resolution callbacks, mutation observer callbacks, and queueMicrotask callbacks are all placed in the microtask queue rather than the callback queue. The critical difference in behavior is that the event loop drains the entire microtask queue after every task from the callback queue completes, and before processing the next callback queue task. This means that if a Promise callback itself schedules another Promise callback, that new microtask is added to the microtask queue and also runs before the next setTimeout callback. You can observe this directly by running setTimeout with zero delay alongside a Promise.resolve callback. The Promise callback always logs first despite appearing later in the code, because it enters the microtask queue while setTimeout enters the lower-priority callback queue.

async and await Under the Hood

The async and await syntax is syntactic sugar over Promises, and understanding this transformation explains the execution order of async code. An async function returns a Promise. The await keyword pauses execution of the async function at that point and yields control back to the event loop, allowing other code to run while the awaited Promise is pending. When the awaited Promise resolves, a microtask is queued to resume execution of the async function from the point after the await expression. This means that even await with an already-resolved Promise does not execute synchronously. The code after the await always runs as a microtask in a future turn of the event loop. This is why code after an await behaves differently from synchronous code directly after a function call, even when the awaited value is immediately available.

Avoiding Event Loop Blocking

Long-running synchronous operations block the event loop, preventing the browser from processing user interactions, running animations, and rendering updates. A JavaScript function that takes more than 50 milliseconds to complete is considered long-running and will produce noticeable jank. For genuinely CPU-intensive tasks, use Web Workers to run the computation in a parallel thread that does not share the call stack with the main thread. For iterating over very large datasets, break the work into smaller chunks using setTimeout or requestIdleCallback to yield control back to the event loop between chunks, allowing user events to be processed in the gaps. The requestAnimationFrame callback runs just before the browser paints the next frame, making it the correct place for visual DOM updates that need to be synchronized with the rendering cycle rather than run on an arbitrary timer interval.

Practical Implications for Debugging

A deep understanding of the event loop transforms your ability to reason about and debug asynchronous code. When a setTimeout callback runs with unexpected values, it is often because the variables it closes over were modified between when the callback was scheduled and when it ran. When Promise chains produce surprising execution orders, drawing out the microtask queue processing order reveals exactly why. The browser DevTools performance panel visualizes the call stack, task queue processing, and frame rendering in a timeline, letting you see exactly when each callback ran and how long it took. The async call stack feature in Chrome DevTools reconstructs the chain of async operations that led to a specific point in code, which is invaluable for debugging Promise chains and async functions that span multiple event loop turns. Profiling async code requires understanding that the performance impact of an async operation can be spread across many separate event loop turns.

Share this article

All posts
#JavaScript#Async#Promise#ES6#Frontend
Abdur Razzak — Full Stack Web Developer
⭐ Top Rated

Upwork Top Rated Developer

Work With a Developer Clients Trust