Some problems are self-similar: "the answer for N depends on the answer for N-1." Counting files inside nested folders, flattening a nested comment thread, or computing factorial all share the same shape. You could solve them with a loop, but the loop version buries the structure in bookkeeping variables. You lose the natural connection between the big problem and the smaller one.
A recursive function is one that calls itself. It trusts that the smaller version of the same problem is already solved, then builds the current answer on top. That trust is not blind — it is backed by a guarantee that each call moves closer to a problem so simple that the answer is obvious.
Every recursive solution needs two parts:
- Base case — the simplest input where you stop recursing and return a value directly.
- Recursive step — the function calls itself with a smaller or simpler input, moving toward the base case.
Forget either one and the function fails. Forget the base case and it recurses without end until the stack overflows. Forget to move toward the base case and the same thing happens. Recursion is not magic — it is disciplined self-reference.
flowchart TD F4["factorial(4)"] --> M1["4 * factorial(3)"] M1 --> M2["3 * factorial(2)"] M2 --> M3["2 * factorial(1)"] M3 --> B["base case: 1"] B --> U1["1"] U1 --> U2["2 * 1 = 2"] U2 --> U3["3 * 2 = 6"] U3 --> U4["4 * 6 = 24"] style F4 fill:#e0900b,color:#fff style B fill:#e0900b,color:#fff
Anatomy of a recursive call
Look at the classic factorial. The mathematical definition is:
- factorial(1) = 1
- factorial(n) = n * factorial(n - 1)
That definition is the code. When factorial(4) runs, JavaScript does not know the answer yet. It pauses, pushes a new frame onto the call stack, and starts factorial(3). That frame pauses for factorial(2), which pauses for factorial(1). Only then does the base case return 1, and the stack unwinds: 2 * 1 = 2, then 3 * 2 = 6, then 4 * 6 = 24.
The key insight is that each call gets its own fresh parameter n. They do not share one variable — the n in the factorial(4) frame is still 4 while the factorial(3) frame works with 3. This isolation is what makes recursion safe; without it, each layer would overwrite the layer below.
function factorial(n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}
console.log(factorial(1));
console.log(factorial(4));
console.log(factorial(5));
The call stack: where the work waits
Every function call creates a stack frame that stores its parameters and local variables. In a normal program the frame is tiny and disappears quickly. In recursion the frames pile up — one per recursive call — and sit there until the base case finally returns.
Imagine a stack of plates. Each recursive call adds a plate. The base case starts removing them. You cannot remove the bottom plate until everything above it is gone. That is why factorial(4) cannot finish its multiplication until factorial(3) finishes, and so on down to factorial(1).
This is also the danger zone. JavaScript limits how deep the stack can go — roughly ten thousand frames, but often far fewer in practice. If your recursion is too deep, or infinite, you get a RangeError: Maximum call stack size exceeded. The fix is never to increase the limit; it is to ensure your base case is reachable.
flowchart TD
subgraph Stack["Call stack"]
S1["factorial(1)\nreturns 1"]
S2["factorial(2)\nwaits for 1"]
S3["factorial(3)\nwaits for 2"]
S4["factorial(4)\nwaits for 3"]
end
S4 --> S3 --> S2 --> S1
style S1 fill:#1e7f3d,color:#fff
style S4 fill:#b45309,color:#fff
Tracing mentally
Before you run a recursive function, trace it on paper. Write the first call, then indent and write the recursive call, and keep going until you hit the base case. Then unwind from the bottom up, substituting each returned value into the line above.
factorial(4)
4 * factorial(3)
3 * factorial(2)
2 * factorial(1)
1
2
6
24
That vertical trace is the exact shape of the call stack. If you cannot draw this tree without confusion, your function is not ready to run. Tracing also reveals a common optimisation: some recursive algorithms redo the same work many times. For now, correctness matters more than speed, but keep the trace habit — it is your best debugging tool.
Recursion on different shapes
Numbers are not the only self-similar data. A string minus its first character is a smaller string. An array minus its first element is a smaller array. That means recursion works just as well for text and lists.
Here is reverseString. The base case is the empty string. The recursive step says: "reverse everything except the first character, then stick that first character on the end."
function reverseString(str) {
if (str === "") return "";
return reverseString(str.slice(1)) + str[0];
}
Trace it mentally: reverseString("hi") becomes reverseString("i") + "h", which becomes (reverseString("") + "i") + "h", which becomes "" + "i" + "h" = "ih". The same stack mechanics apply, only the "smaller" input is a shorter string instead of a smaller number. Once you see this pattern, you can spot recursion candidates everywhere.
function reverseString(str) {
if (str === "") return "";
return reverseString(str.slice(1)) + str[0];
}
console.log(reverseString("hi"));
console.log(reverseString("world"));
console.log(reverseString(""));
flowchart LR
Start["Start with input N"] --> Check{"N is base case?"}
Check -- Yes --> Return["Return fixed value"]
Check -- No --> Step["Compute from f(N-1)"]
Step --> Return
style Check fill:#e0900b,color:#fff
style Return fill:#1e7f3d,color:#fff
function countDown(n) {
if (n <= 0) {
console.log("done");
return;
}
console.log(n);
countDown(n - 1);
}
countDown(3);
What does this print? Watch how the recursive calls unwind.
function factorial(n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}
console.log(factorial(4));
factorial(4) = 4 * factorial(3)
factorial(3) = 3 * factorial(2), and factorial(2) = 2 * factorial(1) = 2.
Write sumRange(n) that returns the sum of every integer from 1 to n inclusive. Use recursion: sumRange(1) is 1, and sumRange(n) is n + sumRange(n - 1).
function sumRange(n) {
// return sum of 1..n using recursion
}
Base case: if n <= 1, return n.
Recursive step: return n + sumRange(n - 1).
function sumRange(n) {
if (n <= 1) return n;
return n + sumRange(n - 1);
}
What does this print? Trace reverseString mentally: the base case is the empty string, and each step slices off the first character.
function reverseString(str) {
if (str === "") return "";
return reverseString(str.slice(1)) + str[0];
}
console.log(reverseString("abc"));
reverseString("abc") becomes reverseString("bc") + "a".
reverseString("bc") becomes reverseString("c") + "b", which becomes (reverseString("") + "c") + "b".
This function is meant to build an array counting down from n to 1, but it crashes with a RangeError because the recursion never stops. Fix it so countDown(3) returns [3, 2, 1], countDown(1) returns [1], and countDown(0) returns [].
function countDown(n) {
if (n <= 0) return [];
return [n].concat(countDown(n));
}
The base case is correct, but the recursive call uses the same n, so it never gets closer to 0.
Change countDown(n) to countDown(n - 1) so each call works with a smaller number.
function countDown(n) {
if (n <= 0) return [];
return [n].concat(countDown(n - 1));
}
Write power(base, exponent) that computes base^exponent using recursion. Any number to the power of 0 is 1. For positive exponents, power(base, exponent) equals base * power(base, exponent - 1).
function power(base, exponent) {
// return base^exponent using recursion
}
Base case: exponent === 0 returns 1.
Recursive step: base * power(base, exponent - 1).
function power(base, exponent) {
if (exponent === 0) return 1;
return base * power(base, exponent - 1);
}
Recap
- A recursive function calls itself, trusting that the smaller version of the problem is already solved.
- Every recursive solution needs a base case (stop condition) and a recursive step that moves toward it.
- Each call creates a new stack frame; frames pile up until the base case returns, then unwind.
- Recursion works on any self-similar structure: numbers, strings, arrays, trees.
- Always verify two things before running: is the base case present, and does every call move strictly closer to it?
Next you'll meet higher-order array methods — map, filter, and reduce — which give you the same power over lists without explicit loops or recursion.