Going DeeperPro· 50 min read

How JavaScript Works: Call Stack, Hoisting & the Event Loop

A peek under the hood — how JavaScript actually runs your code line by line, why some functions seem to exist before you write them, and how async fits in. The #1 interview topic.

What you will learn

  • Describe the call stack and execution context
  • Explain hoisting and why it surprises beginners
  • Sketch the event loop and the order async code runs in

JavaScript does one thing at a time

JavaScript is single-threaded: it has exactly one worker that runs your code, one line at a time, top to bottom. Picture a kitchen with a single cook. To understand any tricky behaviour — odd logging order, async, mysterious undefined — you only need to understand how that one cook is organised.

When JavaScript runs your program, it creates an execution context — think of it as a workspace that holds the variables and the current line for the code running right now. Every time you call a function, a fresh execution context is created for that function’s own variables.

The call stack — a pile of unfinished work

The call stack is the pile of functions currently in progress. When you call a function, it is pushed onto the top of the pile; when it finishes (returns), it is popped off. The cook always works on whatever is on top.

first → second → third, then unwind
<script>
  function third()  { return "done"; }
  function second() { return third(); }
  function first()  { return second(); }
  document.write(first());
</script>
Live preview

Trace the pile as it grows and shrinks:

  1. first() is called → pushed onto the stack. Stack: [first].
  2. Inside first, second() is called → pushed. Stack: [first, second].
  3. Inside second, third() is called → pushed. Stack: [first, second, third].
  4. third returns "done" → popped off. Stack: [first, second].
  5. second returns that value → popped. Then first returns → popped. Stack is empty, and "done" is written.

Note: Output: done (The functions finish in the REVERSE order they were called — last in, first out, just like a stack of plates.)

Watch out: If a function keeps calling itself with no stopping point, the pile grows forever and you hit a "Maximum call stack size exceeded" error — a stack overflow. That is the stack literally running out of room.

Hoisting — why some things exist "early"

Hoisting is JavaScript’s habit of setting up certain names before it runs your code top to bottom. The plain-words version: function declarations are fully available even above where you wrote them, but variables made with let/const are not usable until the line that declares them.

A function works above its definition; let does not
<script>
  // Calling sayHi BEFORE its line works — functions are hoisted
  document.write(sayHi() + "<br>");

  function sayHi() { return "Hello!"; }

  // But this would ERROR — let/const are not ready early:
  // document.write(score);   // ReferenceError
  let score = 10;
  document.write("score is " + score);
</script>
Live preview

sayHi() is called on the first line yet still works, because function declarations are hoisted — JavaScript registers the whole function before running anything. The commented-out score line would crash, because let score is not usable until its own line runs. This is exactly why mixing up declaration order causes confusing bugs.

Note: Output: Hello! score is 10

Tip: Practical takeaway: declare things before you use them. Relying on hoisting makes code confusing. Knowing hoisting exists, though, explains a whole category of "why is this undefined / not defined?" errors.

The event loop — how async waits without freezing

Here is the puzzle that makes this whole lesson worth it. The single cook cannot stand idle waiting for a slow task (a timer, a network request). So slow tasks are handed off to the browser, and their callbacks are queued to run later — only once the call stack is empty. The mechanism that moves queued callbacks back onto the stack is the event loop.

There are even two queues with a priority order: the microtask queue (Promise callbacks) is always emptied before the macrotask queue (setTimeout callbacks). Watch this famous ordering puzzle.

Sync first, then microtasks, then timers
<p id="log"></p>
<script>
  const out = [];
  out.push("1: start");

  setTimeout(() => {
    out.push("4: setTimeout (macrotask)");
    document.getElementById("log").textContent = out.join("  |  ");
  }, 0);

  Promise.resolve().then(() => out.push("3: promise (microtask)"));

  out.push("2: end");
</script>
Live preview

Even though the timer is set to 0 milliseconds, it runs last. Here is why, in order:

  1. "1: start" and "2: end" are plain synchronous lines, so they run immediately, in order, on the call stack.
  2. The setTimeout callback is a macrotask — handed to the browser and queued, never run right away even at 0ms.
  3. The Promise.then callback is a microtask — also queued, but the microtask queue has higher priority.
  4. Once the synchronous code finishes and the stack is empty, the event loop drains all microtasks first → "3: promise" runs.
  5. Only then does it take the next macrotask → "4: setTimeout" runs last.

Note: Output: 1: start | 2: end | 3: promise (microtask) | 4: setTimeout (macrotask) (Synchronous code first, then Promise callbacks, then timers — regardless of the 0ms delay.)

Q. With a setTimeout(fn, 0) and a Promise.then() both queued after synchronous code, which runs first?

Answer: After the synchronous code, the event loop drains the microtask queue (Promise callbacks) before any macrotask (timers), so the Promise.then runs before the setTimeout — even at 0ms.

✍️ Practice

  1. On paper, trace the call stack for three nested function calls and check the unwind order.
  2. Predict the log order of a snippet mixing a console.log, a setTimeout(…,0) and a Promise.then, then run it to confirm.

🏠 Homework

  1. Write a short note, in your own words, explaining the call stack and the event loop to a friend who is new to JavaScript.
Want to learn this with a mentor?

CodingClave runs guided, project-based training (28-day, 45-day & 6-month batches).

Explore Training →