Recursion & the Call Stack

Reviewed & published by Brayan K

Some problems are easiest to solve by having a function call itself. Walking a folder tree, flattening nested arrays, or rendering nested comments are all naturally recursive. The trick that makes it work — and the thing that bites beginners — is the call stack.

Part of the free JavaScript course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

In this lesson you'll see exactly how recursion runs frame by frame, why every recursive function needs a base case, and when to reach for it instead of a loop.

What You'll Learn in This Lesson

🪆 Real-World Analogy: Recursion is like Russian nesting dolls (matryoshka):

🧠 The Two Ingredients of Every Recursion

Every recursive function has two parts:

The simplest possible example is a countdown:

Each call handles one number, then delegates the rest to a smaller call. When n reaches 0, the base case fires and the chain stops.

🧮 Tracing factorial — Watch the Stack Unwind

factorial(n) multiplies every integer from n down to 1. The magic is in how the paused calls resolve backwards:

Here's the call stack growing, then collapsing:

Notice nothing actually multiplies until factorial(1) returns. The four paused frames then resolve top to bottom — that reverse resolution is the call stack at work.

🗂️ The Call Stack, Made Visible

The engine keeps a stack of "frames", one per in-progress call. Each console.log below shows a frame being pushed on the way down and popped on the way up:

All the "entering" lines print first (stack growing), then all the "leaving" lines in reverse (stack shrinking). That mirror-image pattern is the signature of recursion.

🌳 Recursion's Real Power: Nested Data

Counting to a number is cute, but recursion truly earns its keep on data that nests arbitrarily deep — where a loop alone struggles. Flattening a nested array is the classic example:

You don't need to know how deep the nesting goes — each level just recurses one step further. This is the same shape of logic used to walk the DOM, a file system, or nested JSON from an API.

💥 Stack Overflow — The Cautionary Tale

If the base case is missing or never reached, the stack grows without limit until the engine gives up:

The fix is always one of three things: add a reachable base case, make sure every recursive call moves toward it, or convert deep recursion to an iterative loop:

📋 Recursion vs Loop — Quick Guide

Use recursion when…Use a loop when…
Data is nested or tree-shapedData is flat and linear
Depth is unknown (DOM, JSON, files)You know the count up front
Divide-and-conquer (merge sort)Simple accumulation or counting
The recursive version reads clearerDepth could exceed the stack limit
// 1. Countdown with a base case
function countdown(n) {
  if (n <= 0) return console.log("Done!");
  console.log(n);
  countdown(n - 1);
}
countdown(3);

// 2. Factorial
function factorial(n) {
  return n <= 1 ? 1 : n * factorial(n - 1);
}
console.log("5! =", factorial(5));

// 3. Fibonacci (classic but slow — every call splits in two)
function fib(n) {
  if (n < 2) return n;
  return fib(n - 1) + fib(n - 2);
}
console.log("fib(10) =", fib(10));

// 4. Deep sum of nested arrays
function deepSum(arr) {
  return arr.reduce(
    (t, x) => t + (Array.isArray(x) ? deepSum(x) : x),
    0
  );
}
console.log("Deep sum:", deepSum([1, [2, 3, [4]], 5]));

// Try it: write reverseString(str) recursively, or a power(base, exp) function!
// 🎯 YOUR TURN — replace each ___ using the hint beside it.

function countdown(n) {
  // 1) The base case. Without it the function never stops.
  if (n ___ 0) return "liftoff";          // 👉 replace ___ with ===
  console.log(n);
  // 2) The recursive step must move TOWARDS the base case.
  return countdown(n ___ 1);              // 👉 replace ___ with -
}
console.log(countdown(3));

function sumTo(n) {
  if (n <= 0) return 0;
  // 3) This value plus the answer for everything below it.
  return n + ___(n - 1);                  // 👉 replace ___ with sumTo
}
console.log("sumTo(5):", sumTo(5));

const tree = {
  name: "root",
  kids: [{ name: "a", kids: [] }, { name: "b", kids: [{ name: "c", kids: [] }] }],
};
function walk(node, depth = 0) {
  console.log("  ".repeat(depth) + node.name);
  // 4) Each child is a smaller tree, one level deeper.
  for (const kid of node.kids) walk(kid, depth ___ 1);   // 👉 replace ___ with +
}
walk(tree);

// 5) Carrying the answer down in an accumulator instead of building it
//    up on the way back — the shape that keeps the stack flat.
function fact(n, ___ = 1) {               // 👉 replace ___ with acc
  if (n <= 1) return acc;
  return fact(n - 1, acc * n);
}
console.log("fact(6):", fact(6));

// ✅ Expected output:
// 3
// 2
// 1
// liftoff
// sumTo(5): 15
// root
//   a
//   b
//     c
// fact(6): 720

🧩 Reorder Challenge

These lines should define a recursive power(base, exp) and print 2^3 = 8. They are scrambled — reorder them so the base case comes before the recursive case.

B → C → A → D → E

Why: the base case (C) must be checked before the recursive call (A), otherwise exp would keep decreasing past 0 forever and overflow the stack. With exp === 0 returning 1, the calls unwind as 2 * (2 * (2 * 1)) = 8. The function must be fully closed (D) before it can be called (E).

🧠 Quick Recall

1, 2, 3 — in that order. Because the recursive call comes before the console.log, nothing prints on the way down. The logs happen as the stack unwinds, so the smallest n prints first. (Moving the log above the call would print 3, 2, 1.)

2. What does factorial(0) return for n <= 1 ? 1 : n * factorial(n - 1)?

1 — the base case is n <= 1, and 0 satisfies it, so it returns 1 immediately without any recursion. This matches the math definition that 0! equals 1.

3. What error does this eventually throw?

RangeError: Maximum call stack size exceeded. There's no base case and n grows forever, so frames pile up until the stack is full. The fix is a reachable base case that stops the recursion.

⚠️ Common Mistakes to Avoid

Lesson Complete — Recursion & the Call Stack!

Practice quiz

What are the two essential ingredients of every recursive function?

  • A loop and a counter
  • A base case and a recursive case
  • An array and an index
  • A try and a catch

Answer: A base case and a recursive case. Every recursion needs a base case to stop and a recursive case that moves toward it.

What happens if a recursive function has no reachable base case?

  • It returns undefined
  • It loops once and stops
  • It throws RangeError: Maximum call stack size exceeded
  • It returns 0

Answer: It throws RangeError: Maximum call stack size exceeded. Without a reachable base case, frames pile up until the stack overflows with a RangeError.

What is logged? function factorial(n){ return n <= 1 ? 1 : n * factorial(n - 1); } console.log(factorial(4));

  • 10
  • 16
  • 24
  • 120

Answer: 24. factorial(4) = 4 * 3 * 2 * 1 = 24.

What does factorial(0) return for n <= 1 ? 1 : n * factorial(n - 1)?

  • 0
  • 1
  • undefined
  • It throws

Answer: 1. 0 satisfies the base case n <= 1, so it returns 1 immediately (matching 0! = 1).

In which order do recursive frames resolve once the base case is hit?

  • First-in, first-out
  • Last-in, first-out (they unwind in reverse)
  • Random order
  • All at once

Answer: Last-in, first-out (they unwind in reverse). The call stack is LIFO: the deepest call returns first and the frames unwind in reverse.

What does this print? function f(n){ if(n===0) return; f(n-1); console.log(n); } f(3);

  • 3, 2, 1
  • 1, 2, 3
  • 0, 1, 2, 3
  • Nothing

Answer: 1, 2, 3. The recursive call runs before the log, so values print as the stack unwinds: 1, 2, 3.

What does flatten([1, [2, [3, [4, 5]], 6], 7]) return?

  • [1, 2, 3, 4, 5, 6, 7]
  • [1, [2], 7]
  • [7, 6, 5, 4, 3, 2, 1]
  • [1, 2, 6, 7]

Answer: [1, 2, 3, 4, 5, 6, 7]. Recursively flattening descends into every nested array, producing [1,2,3,4,5,6,7].

When is recursion usually a better fit than a loop?

  • Flat, linear data
  • Simple counting
  • Nested or tree-shaped data of unknown depth
  • When depth could exceed the stack limit

Answer: Nested or tree-shaped data of unknown depth. Recursion shines on nested/tree data (DOM, JSON, files); loops suit flat, linear iteration.

A common bug is forgetting to do what with the recursive result?

  • Log it
  • Return it
  • Clone it
  • Stringify it

Answer: Return it. n * factorial(n - 1) must be returned, or the function yields undefined.

Why is naive recursive Fibonacci slow for large n?

  • It uses too much memory per frame
  • Each call splits in two, recomputing the same values exponentially
  • It never reaches the base case
  • It mutates a shared array

Answer: Each call splits in two, recomputing the same values exponentially. Plain fib(n) branches into fib(n-1)+fib(n-2), recomputing subproblems exponentially; memoize or loop.

Continue this course

Frequently asked questions

What is recursion in plain terms?

Recursion is when a function solves a problem by calling itself on a smaller version of the same problem. Each call peels off a layer until it reaches a case so simple it can answer directly — the base case. Then all the paused calls finish in reverse order. It's the same idea as Russian nesting dolls: open one to find a smaller one inside.

What is a base case and why is it essential?

The base case is the condition that stops the recursion — the smallest problem you can answer without calling yourself again. Without it (or if the recursive call never moves toward it), the function calls itself forever and you get a 'Maximum call stack size exceeded' error. Every recursive function needs at least one reachable base case.

What is the call stack and how does recursion use it?

The call stack is the data structure the engine uses to remember which function is running and where to return when it finishes. Each function call pushes a frame on top; each return pops it off. Recursion stacks many frames of the same function — one per call — and they unwind in last-in, first-out order once the base case is hit.

When should I use recursion instead of a loop?

Recursion shines for naturally nested or tree-shaped data: file systems, the DOM, nested JSON, or divide-and-conquer algorithms like merge sort. Loops are usually better for flat, linear iteration and avoid call-stack limits. Any recursion can be rewritten as a loop and vice versa; choose whichever expresses the problem most clearly.

What causes a stack overflow and how do I avoid it?

A stack overflow happens when recursion goes too deep — either a missing/unreachable base case (infinite recursion) or legitimately huge depth. Fix it by ensuring the base case is always reached, reducing the input toward it on every call, or rewriting deep recursion as an iterative loop or with an explicit stack.