The Single-Threaded Illusion
The Components of the Concurrency Model
- The Call Stack: This is where the V8 engine (or whichever JS engine you use) keeps track of what function is currently executing. If a function calls another function, it gets pushed onto the top of the stack. When a function returns, it's popped off the stack.
- Web APIs (Browser) / C++ APIs (Node.js): JavaScript itself cannot make an HTTP request or set a timer. These are features provided by the environment. When you call setTimeout or fetch, JavaScript passes the callback function to the Web API and continues executing the next line of code in the Call Stack. It does not wait.
- The Callback Queue (Task Queue): When a Web API finishes its job (e.g., the timer expires, or the HTTP request returns), it doesn't push the callback immediately back onto the Call Stack. That would interrupt whatever code is currently running. Instead, it pushes the callback into the Callback Queue.
- The Microtask Queue: This is a special, high-priority queue specifically for Promises (.then, .catch, async/await) and MutationObserver.
The Event Loop in Action
- Is the Call Stack empty?
- Is there anything in the Queues?
- Execute all code currently in the Call Stack until it is completely empty.
- Check the Microtask Queue (Promises). If there are tasks here, push them onto the Call Stack one by one and execute them until the Microtask Queue is completely empty.
- Check the Callback Queue (Macrotasks like setTimeout, DOM events). If there are tasks here, take the first one, push it onto the Call Stack, and execute it.
- Repeat the process.
A Code Example
Javascript
console.log('1. Start');
setTimeout(() => {
console.log('2. Timeout');
}, 0);
Promise.resolve().then(() => {
console.log('3. Promise');
});
console.log('4. End');
- console.log('1. Start') goes on the Call Stack and runs immediately.
- setTimeout goes on the Call Stack. The browser takes the callback, sets a timer for 0ms, and pops setTimeout off the stack. Because it's 0ms, the callback immediately goes to the Callback Queue.
- Promise.resolve().then() goes on the Call Stack. The callback goes to the Microtask Queue.
- console.log('4. End') goes on the Call Stack and runs immediately.
- The Call Stack is now empty. The Event Loop checks the Microtask Queue. It finds the Promise callback, moves it to the Call Stack, and executes console.log('3. Promise').
- The Call Stack is empty again. The Microtask Queue is empty. The Event Loop checks the Callback Queue. It finds the Timeout callback, moves it to the Call Stack, and executes console.log('2. Timeout').