Recursion & the Call Stack

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 .

Learn Recursion & the Call Stack in our free JavaScript course — a beginner-friendly interactive lesson with runnable examples, a practice exercise and a…

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.

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

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.

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

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.

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:

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.

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).

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.

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.

Up next: ES6+ Features — the modern syntax that makes JavaScript a joy to write! ✨

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?

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.