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
- The base case and the recursive case
- How the call stack pushes and pops frames
- Tracing factorial and countdown step by step
- Recursion on nested data (sum & flatten)
- Why stack overflows happen
- Choosing recursion vs a loop
🪆 Real-World Analogy: Recursion is like Russian nesting dolls (matryoshka):
- • Each doll contains a smaller doll — the same problem, one size down
- • You keep opening until you reach the tiniest solid doll — the base case
- • Then you close them back up in reverse — the call stack unwinding
- • Forget that final solid doll and you'd open forever — that's infinite recursion
🧠 The Two Ingredients of Every Recursion
Every recursive function has two parts:
- A base case — the simplest input it answers directly, with no further calls.
- A recursive case — it does a little work, then calls itself on a smaller input that moves toward the base case.
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-shaped | Data 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 clearer | Depth 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
- 1️⃣ No base case. Every recursive function needs a condition that stops it, or it overflows the stack.
- 2️⃣ Not shrinking the input. Each call must move toward the base case (e.g. n - 1), not stay the same or grow.
- 3️⃣ Forgetting to return the recursive result. n * factorial(n - 1) must be returned, or you'll get undefined.
- 4️⃣ Naive Fibonacci on big inputs. Plain fib(n) recomputes the same values exponentially — memoize or loop for large n.
Lesson Complete — Recursion & the Call Stack!
- ✅ Every recursion needs a base case and a shrinking recursive case
- ✅ The call stack pushes a frame per call and unwinds last-in, first-out
- ✅ Recursion is ideal for nested/tree data; loops suit flat iteration
- ✅ A missing or unreachable base case causes a stack overflow
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
- Previous: Generators (function*, yield)
- Next: Checkpoint: Core JavaScript — Review & combine the core concepts in a build challenge — then a quiz
- Quick reference: JavaScript cheat sheet
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.