JavaScript has a concurrency model based on an event loop, which is responsible for executing the code, collecting and processing events, and executing queued sub-tasks. This model is what allows a single-threaded language to be non-blocking.
Let's break down the key components:
- Call Stack: This is where function calls are tracked. When you call a function, it's pushed onto the stack. When the function returns, it's popped off. JavaScript can only do one thing at a time, so there's only one call stack.
- Web APIs: The browser provides APIs for asynchronous operations (e.g., setTimeout, DOM events, fetch). When you call one of these, the browser's Web API handles it. It's not part of the JavaScript engine itself.
- Callback Queue (or Task Queue): When a Web API finishes its task (e.g., a timer expires or data arrives), the callback function associated with it is placed in the Callback Queue.
- Event Loop: This is the star of the show. The event loop has one simple job: it continuously checks if the Call Stack is empty. If it is, it takes the first item from the Callback Queue and pushes it onto the Call Stack for execution.
Code Snippet: Seeing the Event Loop in Action
JavaScript
console.log('Start'); // 1. Pushed to stack, logs 'Start', popped off.
setTimeout(() => {
  // 2. setTimeout is a Web API. The browser starts a 0ms timer.
  //    The callback function is NOT immediately queued.
  console.log('Timeout Callback'); // 5. Finally, this is pushed to the stack and logs.
}, 0);
Promise.resolve().then(() => {
  // 3. The Promise's .then() callback is placed in the Microtask Queue.
  console.log('Promise Resolved'); // 4. This logs before the timeout.
});
console.log('End'); // 3. Pushed to stack, logs 'End', popped off.
// At this point, the main script is done, and the call stack is empty.
// The Event Loop now checks the queues.
Microtasks vs. Macrotasks
This brings us to a finer point. The "Callback Queue" is actually two queues:
- Microtask Queue: This queue is for promise callbacks (.then(), .catch(), .finally) and other high-priority tasks. The Microtask Queue is processed after each task and before rendering.
- Macrotask Queue (or Task Queue): This is the original Callback Queue we discussed. It's for tasks like setTimeout, setInterval, and I/O operations.
The event loop's rule is: after a macrotask finishes, process ALL available microtasks before moving on to the next macrotask. This is why the Promise Resolved message logs before Timeout Callback in our example, even though the timeout was set to 0ms. Promises have higher priority.