JavaScript is famously single-threaded — it executes one piece of code at a time in a synchronous manner. Yet, it handles asynchronous operations like timers, network requests, and DOM events gracefully without blocking. The secret behind this magic is the event loop, a core part of the JavaScript runtime that orchestrates concurrency.
In this article, we’ll explore how JavaScript’s event loop, call stack, and the two kinds of task queues — the microtask queue and macrotask queue — work together to enable asynchronous behavior in a single-threaded environment. By understanding this mechanism, you’ll be able to write more efficient and predictable async code.
The Call Stack: JavaScript’s Execution Engine
The call stack is at the heart of JavaScript’s execution model. Think of it like a stack of plates: when a function is called, it gets pushed onto the stack, and when it returns, it’s popped off.
- The call stack tracks what function is currently executing.
 - Only one function runs at a time — JavaScript is single-threaded.
 - If the stack is empty, the event loop checks the queues for more work to do.
 
Web APIs and the Macrotask Queue
JavaScript doesn’t run alone — it’s embedded in a host environment like a browser or Node.js, which provides asynchronous capabilities via Web APIs (like setTimeout, fetch, or DOM events).
- When you call an async API, it’s handled outside the call stack by the Web API environment.
 - Once completed, the callback is queued in the macrotask queue (a.k.a. the callback queue).
 - The event loop picks the next macrotask only after the call stack is empty and all microtasks are processed.
 
The Microtask Queue: Promises and More
The microtask queue (also known as the job queue) is another queue with higher priority than the macrotask queue.
- Microtasks include 
.then()callbacks of Promises,queueMicrotask, andprocess.nextTick(Node.js). - After each task (macrotask or initial script) completes and the call stack clears, the event loop drains the microtask queue — running all its callbacks before touching the next macrotask.
 - This is why Promises often feel “faster” than 
setTimeout. 
How the Event Loop Works
- Run the script’s synchronous code on the call stack.
 - When the call stack is empty:
- Run all microtasks from the microtask queue.
 - Then, take the next macrotask from the macrotask queue and push it onto the stack.
 
 - Execute the macrotask.
 - Repeat this cycle endlessly.
 
This system is what allows JavaScript to be asynchronous without the complexity of multithreading.
Event Loop in Action: A Simple Example
console.log("Start");
setTimeout(() => {
  console.log("Macrotask: setTimeout");
}, 0);
Promise.resolve().then(() => {
  console.log("Microtask: Promise");
});
console.log("End");Step-by-step execution:
console.log("Start")→ prints StartsetTimeoutschedules a callback in the macrotask queue- Promise 
.then()schedules a callback in the microtask queue console.log("End")→ prints End- Call stack is now empty → event loop processes microtasks → prints Microtask: Promise
 - Microtask queue is empty → event loop processes macrotasks → prints Macrotask: setTimeout
 
Output:
Start
End
Microtask: Promise
Macrotask: setTimeoutSummary: JavaScript’s Asynchronous Magic
- The call stack runs synchronous code.
 - Web APIs handle async operations and enqueue callbacks in the macrotask queue.
 - Promises and similar features enqueue callbacks in the microtask queue, which always runs first.
 - The event loop coordinates all of this, ensuring a non-blocking experience despite JavaScript’s single-threaded nature.
 
Understanding how the event loop prioritizes tasks helps avoid surprises and lets you harness async JavaScript more effectively.