The one-line definition: a function that calls itself until it hits a stopping condition.
That's it. Everything else is just patterns.
An example: Russian dolls (matryoshka)
You open a Russian doll. Inside is a smaller doll. Open it → another smaller doll inside. Keep opening until you find the tiniest doll with nothing inside. Then you know the answer: "there were 5 dolls."
You could write that as a recipe:
openDoll(doll):
if doll is empty inside:
return 1 ← the stopping condition
else:
return 1 + openDoll(inside doll) ← the function calls itself
Two ingredients every recursion needs:
-
Base case — when to stop (
if doll is empty). -
Recursive step — call yourself on a smaller version of the problem (
openDoll(inside doll)).
If you forget the base case, you recurse forever and crash. That is the #1 recursion bug.
Concrete: countdown from 3
Easiest possible recursion in JavaScript:
function countdown(n) {
if (n === 0) { // base case
console.log("done!");
return;
}
console.log(n); // do work
countdown(n - 1); // call itself with a smaller n
}
countdown(3);
What gets printed:
3
2
1
done!
Trace it yourself
Imagine the function is a sticky note. Every time it calls itself, you grab a NEW sticky note and start it. Old sticky notes wait, paused, until the new one finishes.
countdown(3) starts ────────────────────┐
prints "3" │
calls countdown(2) ─────────────┐ │ ← waiting
│ │
countdown(2) starts │ │
prints "2" │ │
calls countdown(1) ──┐ │ │
│ │ │
countdown(1) starts│ │ │
prints "1" │ │ │
calls countdown(0)│ │ │
│ │ │ │
countdown(0) starts ←─ smallest, deepest call
prints "done!"
returns → back to countdown(1)
countdown(1) returns → back to countdown(2)
countdown(2) returns → back to countdown(3)
countdown(3) returns → all done
Notice: every call waits for the next one to finish. Sticky notes pile up until the smallest one finishes. Then they unwind in reverse order.
That pile of paused calls is what the computer calls the call stack.
Loop vs recursion (same job)
The countdown above could also be a loop:
function countdown(n) {
for (let i = n; i > 0; i--) {
console.log(i);
}
console.log("done!");
}
Same output. But:
-
Loop: one variable
i, one execution context. Memory usage is constant. -
Recursion: one execution context PER call. Calling
countdown(1000000)would crash with "Maximum call stack size exceeded" because a million sticky notes don't fit in memory.
When recursion shines
Recursion is great when the problem is naturally nested:
- Walk a folder tree (folders inside folders inside folders).
- Parse JSON / HTML (objects inside objects).
- Solve a maze (try a path, if dead end, back up and try another).
- Math: factorial, Fibonacci, tree traversals.
Anything where "the smaller version of the problem looks just like the bigger version" is a good fit.
When to avoid recursion
- Long flat sequences (counting from 1 to a million, walking pages of an API). A loop is faster and safer.
- Anywhere the depth could be unbounded by user input. Loop or you risk a stack overflow.
- Languages without tail-call optimisation (JavaScript / Node falls here). Even "tail recursion" still grows the stack in V8.
The mental shortcut
| Loop | Recursion |
|---|---|
| One sticky note, reused N times | N sticky notes, all alive at once until the deepest one finishes |
| Constant memory | Memory grows with depth |
| Best for: flat sequences | Best for: nested / tree-shaped data |
Recursion = a function that calls itself, with a base case to stop.
The cost is paid in stack space — every call lives until the inner one returns.
Top comments (0)