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.