Understanding Recursion: How I Finally Understood It
A personal, practical breakdown of recursion for developers who've struggled to get it to click.

Understanding recursion can feel strange because the function calls itself before the first call is finished. For a lot of developers, the code looks short but the mental model feels slippery. The trick is to stop thinking of recursion as magic and start thinking of it as a stack of unfinished tasks.
Once that clicked for me, recursion became less scary.
Understanding Recursion: The Aha Moment
I used to read recursive functions from top to bottom like normal sequential code, then get confused when the answer seemed to appear from nowhere. The shift came when I started tracing each call as a paused frame waiting for a smaller answer.
That mental model changed the whole topic. Instead of asking “how does the function already know the answer?” I started asking “what smaller version of the problem is this call waiting for?” Once you can answer that, recursion becomes easier to debug.
The Two Parts of Recursion
Every recursive function needs two pieces:
- A base case
- A recursive case
The base case stops the function. The recursive case moves the problem closer to the base case.
Here is factorial:
function factorial(n: number): number {
if (n <= 1) {
return 1;
}
return n * factorial(n - 1);
}
factorial(5) means:
5 * factorial(4)
5 * 4 * factorial(3)
5 * 4 * 3 * factorial(2)
5 * 4 * 3 * 2 * factorial(1)
At factorial(1), the function returns 1. Then the paused calls resolve backward.
Tracing the Call Stack
The call stack is the list of active function calls waiting to finish.
For factorial(4), the stack builds like this:
factorial(4)
factorial(3)
factorial(2)
factorial(1)
Then it unwinds:
factorial(1) returns 1
factorial(2) returns 2 * 1 = 2
factorial(3) returns 3 * 2 = 6
factorial(4) returns 4 * 6 = 24
That is recursion: reduce the problem, hit the base case, then combine the answers on the way back.
The Base Case Mistake
Forgetting the base case causes infinite recursion until the runtime stops you.
function brokenCountdown(n: number): void {
console.log(n);
brokenCountdown(n - 1);
}
This never stops. A safe version includes a base case:
function countdown(n: number): void {
if (n < 0) {
return;
}
console.log(n);
countdown(n - 1);
}
The base case is not optional. It is the exit door.
Recursion vs Iteration
Many recursive solutions can be written iteratively.
function factorialIterative(n: number): number {
let result = 1;
for (let value = 2; value <= n; value += 1) {
result *= value;
}
return result;
}
The iterative version uses a loop and a variable. The recursive version uses the call stack.
Use recursion when the problem naturally breaks into smaller versions of itself:
- Trees
- Graph traversal
- Backtracking
- Divide and conquer
- Nested structures
Use iteration when a simple loop is clearer and avoids stack depth issues.
A Better Mental Model
Do not ask, “How does the whole recursion finish?” immediately.
Ask:
- What is the smallest input I can answer directly?
- How do I reduce the current input toward that smallest input?
- How do I combine the smaller answer with the current step?
If you can answer those three, the recursive shape becomes easier to write.
Related Posts
- Big O Notation for Beginners: Explained Simply With Code
- How to Approach Coding Interview Questions Without Panicking
If recursion still feels foggy, practice by tracing with pen and paper. The goal is not speed first. The goal is learning to trust each smaller call.
