JavaScript Event Loop

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.

event-loop.js
console.log("First");
console.log("Second");
console.log("Third");
// First
// Second
// Third

Let’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.

event-loop.js
function greet() {
console.log("Hello");
}
function start() {
greet();
}
start();
// Hello

Let’s trace how the stack grows and shrinks as this code runs:

  • start() is called, so start goes on the stack.
  • Inside start, we call greet, so greet goes on top of start.
  • greet runs console.log("Hello") and prints Hello, then finishes and leaves the stack.
  • With greet gone, start finishes 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:

event-loop.js
console.log("1");
setTimeout(() => {
console.log("2");
}, 0);
console.log("3");
// 1
// 3
// 2

Even though the timer is set to 0 milliseconds, 2 prints last. Here is why, step by step.

  1. console.log("1") runs right away on the call stack and prints 1.
  2. setTimeout hands its callback to the timer. Even with 0, the callback does not run now. It is sent to the callback queue to wait.
  3. console.log("3") runs next on the stack and prints 3.
  4. 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).

event-loop.js
console.log("1");
setTimeout(() => console.log("2"), 0); // macrotask
Promise.resolve().then(() => console.log("3")); // microtask
console.log("4");
// 1
// 4
// 3
// 2

Let’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 prints 1.
  • setTimeout sends 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 prints 4, 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 2 last.

⚠️ 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!

  1. Run console.log("1"); setTimeout(() => console.log("2"), 0); console.log("3"); and confirm it prints 1, 3, 2.
  2. Add a Promise.resolve().then(() => console.log("P")); line and predict where P prints.
  3. Change the setTimeout delay from 0 to 1000 and see that the order does not change.
  4. Write a long for loop before a console.log and 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") prints 1, 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.

Share & Connect