Recursion
Recursion is when a function calls itself to solve a smaller version of the same problem. Done right, it turns complex problems into elegant, readable solutions. Done wrong, it causes stack overflows and exponential slowdowns. The key is understanding why it works and how to control it.
The Two Required Pieces
Every correct recursive function has exactly two things:
- Base case β the condition where the function stops and returns a value directly, without calling itself again.
- Recursive case β the step where the function calls itself with a smaller or simpler input, moving toward the base case.
If youβre missing the base case, you get infinite recursion. If your recursive case doesnβt actually shrink the problem, you get infinite recursion. Both crash with a stack overflow.
WARNING
JavaScript and TypeScript have a call stack limit (typically around 10,000β15,000 frames). Recursion on very large inputs will hit this limit. Always ask: could this be a loop instead?
Factorial Example
The factorial of n (written n!) is n Γ (nβ1) Γ (nβ2) Γ β¦ Γ 1. By definition, 0! = 1.
function factorial(n: number): number {
// Base case
if (n <= 1) return 1;
// Recursive case: n! = n Γ (n-1)!
return n * factorial(n - 1);
}
// factorial(4) calls:
// 4 * factorial(3)
// 3 * factorial(2)
// 2 * factorial(1)
// 1 β base case
// 2 * 1 = 2
// 3 * 2 = 6
// 4 * 6 = 24
Fibonacci and the Problem with Naive Recursion
The Fibonacci sequence is defined as: F(0) = 0, F(1) = 1, F(n) = F(nβ1) + F(nβ2).
// Naive β O(2^n) time due to redundant calls
function fib(n: number): number {
if (n <= 1) return n;
return fib(n - 1) + fib(n - 2);
}
This is correct but catastrophically slow. fib(40) makes over a billion recursive calls because it recomputes the same subproblems repeatedly.
Memoization: Caching Recursive Results
Memoization stores the result of each call so we never compute the same value twice.
function fibMemo(n: number, memo: Map<number, number> = new Map()): number {
if (n <= 1) return n;
if (memo.has(n)) return memo.get(n)!;
const result = fibMemo(n - 1, memo) + fibMemo(n - 2, memo);
memo.set(n, result);
return result;
}
// Now O(n) time and O(n) space β each value computed exactly once
NOTE
Memoization is the bridge between recursion and dynamic programming. If a recursive solution has overlapping subproblems, add a cache and youβve written a top-down DP solution.
The Call Stack
Each function call pushes a stack frame onto the call stack, which holds that callβs local variables and return address. When the function returns, the frame is popped. Recursion builds a chain of frames that unwind once the base case is reached.
Call stack for factorial(3):
βββββββββββββββββββ β top (most recent)
β factorial(1) β
βββββββββββββββββββ€
β factorial(2) β
βββββββββββββββββββ€
β factorial(3) β
βββββββββββββββββββ β bottom (first call)
Tail Recursion
A function is tail recursive when the recursive call is the very last operation β no work happens after it returns. Some languages and runtimes optimize tail calls to avoid growing the stack. JavaScript does not reliably perform this optimization, so in JS/TS, prefer iteration for large inputs even when the recursive form is elegant.
TIP
Think of recursion as a natural fit for problems with a tree-like structure (file systems, DOM trees, nested data) where the depth is bounded and reasonable. For large linear problems (summing a million items), use a loop.
Further Learning
Search these terms to go deeper:
- βRecursion CS50 lecture David Malanβ β intuitive visual explanation with live tracing
- βThe Recurse Book by Eric Robertsβ β gentle introduction focused on problem-solving patterns
- βTail call optimization JavaScript MDNβ β details on what JS actually optimizes (and what it doesnβt)
- βRecursion practice problems leetcode easyβ β hands-on problems to build pattern recognition