JavaScript Event Loop
Table of Contents + −
In the previous lesson, we learned how JavaScript moves declarations to the top of their scope before running. Now let’s see how JavaScript runs your code in order, and how it handles slow tasks without freezing.
🧵 JavaScript Is Single-Threaded
JavaScript does one thing at a time. It has a single thread, which means it runs one piece of code, finishes it, and only then moves to the next. There is no second worker running code alongside it.
This sounds like a problem. If JavaScript can only do one thing at a time, how does a web page stay responsive while waiting for a network request that takes two seconds? The answer is the event loop.
console.log("First");console.log("Second");console.log("Third");// First// Second// ThirdLet’s walk through how JavaScript runs these three lines:
- The first line runs and prints
First. - Only after that finishes does the second line run and print
Second. - Then the third line runs and prints
Third.
This is synchronous code: each line waits for the one before it to finish, so the output follows the exact order you wrote it.
📚 The Call Stack
The call stack is the list of tasks JavaScript is working on right now. When a function is called, it is added to the top of the stack. When the function finishes, it is removed from the top. JavaScript always runs whatever is on top of the stack.
function greet() { console.log("Hello");}
function start() { greet();}
start();// HelloLet’s trace how the stack grows and shrinks as this code runs:
start()is called, sostartgoes on the stack.- Inside
start, we callgreet, sogreetgoes on top ofstart. greetrunsconsole.log("Hello")and printsHello, then finishes and leaves the stack.- With
greetgone,startfinishes and leaves the stack too.
The stack runs synchronous code, and it runs it fast.
⏳ The Callback Queue
Some tasks are not instant. A timer set with setTimeout, a network request with fetch, or a button click all take time or happen later. JavaScript does not stop and wait for these. It hands them off, keeps running the rest of your code, and the slow task waits in the callback queue until it is ready.
The callback queue is a waiting line. When an async task is done, its callback joins the back of the line. The event loop checks one rule: the call stack must be completely empty before anything from the queue is allowed to run.
The event loop in one sentence
The event loop watches the call stack. When the stack is empty, it takes the next callback from the queue and puts it on the stack to run.
🔢 Why 1, 3, 2?
Here is the classic example that surprises every beginner:
console.log("1");
setTimeout(() => { console.log("2");}, 0);
console.log("3");// 1// 3// 2Even though the timer is set to 0 milliseconds, 2 prints last. Here is why, step by step.
console.log("1")runs right away on the call stack and prints1.setTimeouthands its callback to the timer. Even with0, the callback does not run now. It is sent to the callback queue to wait.console.log("3")runs next on the stack and prints3.- Now the synchronous code is done and the call stack is empty. The event loop takes the waiting callback from the queue and runs it, printing
2.
The 0 does not mean “run immediately.” It means “run as soon as possible, but only after the current synchronous code finishes and the stack is empty.”
⚡ Microtasks and Macrotasks
Not every waiting task has the same priority. There are two queues, and one always wins.
| Type | Examples | Priority |
| Microtasks | Promise callbacks (.then, await) | Run first |
| Macrotasks | setTimeout, setInterval, events | Run after |
When the call stack is empty, the event loop drains all microtasks before it touches a single macrotask. That is why a resolved promise beats a setTimeout(..., 0).
console.log("1");
setTimeout(() => console.log("2"), 0); // macrotask
Promise.resolve().then(() => console.log("3")); // microtask
console.log("4");// 1// 4// 3// 2Let’s follow this code line by line to see why the output is 1, 4, 3, 2:
console.log("1")runs on the stack right away and prints1.setTimeoutsends its callback to the macrotask queue to wait.Promise.resolve().then(...)sends its callback to the microtask queue to wait.console.log("4")runs on the stack and prints4, finishing the synchronous code.- The stack is now empty, so the event loop drains the microtask queue first and prints
3. - Only after the microtasks are done does the macrotask run, printing
2last.
⚠️ Common Mistakes to Avoid
| Mistake | Problem | Solution |
Expecting setTimeout(fn, 0) to run instantly | It waits until the stack is empty, so it runs later | Put code that must run after it inside the callback |
| Blocking the loop with a heavy synchronous loop | The page freezes and nothing else can run | Break up the work or move it off the main thread |
| Assuming async means parallel threads | JavaScript still runs one thing at a time | Think of it as taking turns, not running at once |
Keep the stack short
A long synchronous loop blocks everything, including the queue. If the stack never empties, no callback ever gets to run.
🔧 Try It Yourself!
- Run
console.log("1"); setTimeout(() => console.log("2"), 0); console.log("3");and confirm it prints1,3,2. - Add a
Promise.resolve().then(() => console.log("P"));line and predict wherePprints. - Change the
setTimeoutdelay from0to1000and see that the order does not change. - Write a long
forloop before aconsole.logand notice the page pauses until the loop finishes.
🧩 What You’ve Learned
- ✅ JavaScript is single-threaded and does one thing at a time
- ✅ The call stack runs synchronous code from top to bottom
- ✅ Async callbacks wait in the queue and run only when the stack is empty
- ✅
console.log("1"); setTimeout(..., 0); console.log("3")prints1,3,2 - ✅ Microtasks (promises) run before macrotasks (
setTimeout)
🚀 What’s Next?
Now that you understand the event loop, we will use timers to control how often a function runs. Let’s continue to Debouncing.