Recursion
Learn how JavaScript functions call themselves, why every recursive function needs a base case, how the call stack grows and unwinds, and when recursion is clearer than a loop.
- 01Read a recursive functionSeparate the base case from the recursive case and predict simple results like factorial.
- 02Explain the stackWatch calls push onto the call stack and pop off when they return.
- 03Choose recursion or a loopUse recursion for naturally nested data, and know when iteration is safer.
Functions that call themselves
Recursion means a function calls itself to solve a smaller version of the same problem. The idea sounds circular at first, but you already know the shape from everyday life: solve a big thing by opening a smaller thing inside it, then a smaller one, until you reach the smallest piece that needs no more opening.
A recursive function always has two jobs. First, it needs a direct answer for the smallest input. That is the base case. Second, it needs a way to move every larger input toward that smallest input. That is the recursive case. If either part is missing, the function either stops too soon or never stops.
Recursion is a function saying: “If this is the simple case, answer now. Otherwise, call myself with a smaller case and use that answer.”
Open a nesting doll and you find a smaller doll with the same shape. Open that one and you find another. Eventually you reach the tiny doll that does not open. That last doll is what makes the whole game finite: without it, you would keep opening forever.
- In real life: The biggest doll
- In JavaScript: The first function call, like
factorial(4) - In real life: A smaller doll inside
- In JavaScript: The recursive call, like
factorial(3) - In real life: The smallest doll that does not open
- In JavaScript: The base case, like
factorial(1) - In real life: Closing the dolls back up
- In JavaScript: Returning from each call and combining the results
Where the analogy stops: Real dolls wait quietly. Function calls use memory on the call stack while they wait, so a program can run out of stack space if it opens too many dolls.
This lesson is still beginner-level. We will use tiny numbers and a small folder tree so the stack stays visible. Later, the deeper lessons The call stack, Stack limits & stack overflow, Tail calls, and Advanced recursion revisit the same ideas with engine-level detail.
Base & recursive cases
STEP THROUGHThe classic first recursive example is factorial. In math, 4! means 4 × 3 × 2 × 1. Written as a function, factorial(4) can ask for factorial(3), then multiply that answer by 4.
Read the two important lines before stepping:
if (n === 1) return 1;is the base case. It answers immediately and makes recursion stop.return n * factorial(n - 1);is the recursive case. It calls the same function with a smaller number, then multiplies when the smaller answer comes back.
Predict the answer, then step. Watch the stack grow until the base case, then shrink as each return multiplies back up.
script
function factorial(n) { if (n === 1) { return 1; } return n * factorial(n - 1);} console.log(answer);Notice the order. JavaScript cannot finish 4 * factorial(3) until factorial(3) returns. That call cannot finish until factorial(2) returns, and so on. The base case is not a decoration; it is the first answer the waiting calls can build on.
if (n === 1) return 1;return n * factorial(n - 1);if (!node.children) return 1;total = total + countFiles(child);if (items.length === 0) return 0;return sum(rest) + first;
Sort each line by its job. Base cases answer directly; recursive cases call the same function again.
The call stack: plates that wait
INTERACTIVEEvery function call needs a place to remember its parameters, local variables, and where to continue after the call returns. JavaScript tracks those waiting calls with the call stack. When a function is called, a stack frame is pushed on top. When it returns, that frame pops off and the caller underneath continues.
Imagine washing plates and stacking them. You can only add to the top and take from the top. Recursive calls work the same way. The newest call is on top, and the older calls wait underneath.
- In real life: Put a plate on top
- In JavaScript: Call a function and push a stack frame
- In real life: Only the top plate is active
- In JavaScript: The most recent call runs now
- In real life: Take the top plate off
- In JavaScript: Return from that function
- In real life: The plate underneath is revealed
- In JavaScript: The caller continues where it paused
Where the analogy stops: Real plates can be removed from the middle if you are careful. The call stack is stricter: JavaScript always returns from the top call first.
This visualizer records a real fib(4) run. Fibonacci is intentionally repetitive: fib(4) asks for fib(3) and fib(2), and those calls ask for smaller calls again. That repetition is why later lessons introduce memoization, a way to remember answers instead of recomputing them.
function fib(n) { if (n <= 1) return n; return fib(n - 1) + fib(n - 2);} const answer = fib(4);Calls made so far: 1
fib(4) was called, so a new plate goes on top. fib(4) makes 9 total calls because fib repeats smaller questions.
The player shows recorded events from instrumented lesson code. It is honest about the call order and values, but it is not your browser’s debugger. The published Debugging in the browser lesson shows the real Call Stack panel.
Stack overflow: when recursion never reaches the smallest doll
REAL ERRORRecursion without a reachable base case is like standing between two mirrors: the reflections keep going. In code, each reflection is another function call. The stack grows until the engine refuses to add another frame.
Face two mirrors toward each other and you see reflection after reflection. There is no “smallest reflection.” A recursive function with no base case has the same problem: it keeps asking itself again.
- In real life: Two mirrors reflect forever
- In JavaScript: A function calls itself without getting closer to a base case
- In real life: The plate tower hits the ceiling
- In JavaScript: The call stack reaches its limit
- In real life: You stop stacking
- In JavaScript: The engine throws an error instead of crashing the whole page
Where the analogy stops: A mirror image is harmless. Runaway recursion uses real memory and time, so the engine must stop it.
Most V8-based browsers and Node.js report this as RangeError: Maximum call stack size exceeded. Other engines can use different wording, such as “too much recursion.” Click the button to run a tiny unsafe function inside try/catch. The page catches the error and shows the approximate depth reached in your browser, which varies by engine and settings.
let depth = 0;function noBaseCase() { depth = depth + 1; return noBaseCase();} try { noBaseCase();} catch (error) { console.log(error.name); console.log(depth > 0);}- error name—approx depth—recoverable?click Run
This deliberately missing base case runs only when you click. try/catch catches the error so the page can keep going.
The fix is not “make the limit bigger.” The fix is to make sure every recursive step moves toward the base case, or to rewrite the algorithm as a loop when the depth can be large.
Recursion vs loops
COMPARERecursion and loops can solve many of the same problems. The earlier Loops: while & for lesson showed how to repeat work with one frame that changes a counter. Recursion repeats work by making new function calls. Both are useful, but they have different costs.
function sumToRecursive(n) { if (n === 0) return 0; return n + sumToRecursive(n - 1);} function sumToLoop(n) { let total = 0; for (let current = n; current > 0; current = current - 1) { total = total + current; } return total;}- sumToRecursive(5)15sumToLoop(5)15
Both versions return 15. The recursive version uses 6 calls; the loop keeps one frame and changes total.
| Question | Recursion | Loop |
|---|---|---|
| What changes? | Each call gets new parameters and waits for smaller calls | One frame updates variables each trip through the loop |
| Best fit | Nested or self-similar data: trees, folders, menus | Flat repetition: count, scan a list, retry a fixed number of times |
| Risk | Too many calls can overflow the stack | Infinite loops can freeze work if the condition never changes |
| Performance | Usually more overhead because calls allocate frames | Usually faster and stack-safe |
| Tail calls | Do not rely on optimization in everyday JavaScript; Safari’s JavaScriptCore implements proper tail calls, most engines do not | No tail-call optimization needed |
A good beginner rule: if the data is flat, try a loop first. If the data contains smaller versions of itself, recursion may read like the problem statement. The later Tail calls lesson covers why portable JavaScript should not rely on tail-call optimization.
- Walk every file in a folder tree
- Add the numbers 1 through 100
- Render comments with replies inside replies
- Print 10, 9, 8… once
- Search a menu where each item may have children
- Total the prices in one flat array
Choose the clearer tool for each scenario. There can be many working solutions; this sorter rewards the usual beginner-friendly choice.
Recursive data such as trees
INTERACTIVERecursion shines when your data is recursive too. A folder can contain files and more folders. A comment can have replies, and each reply can have replies. A menu item can have children, and each child can have children. That shape is called a tree.
Stage 3 has full lessons on objects and arrays. For this lesson, read the object literal as a tiny model: folders have a children array, and files have a size. The recursive function does not need to know how deep the tree is. It only knows how to handle one node: if it has no children, count one file; otherwise count each child and add the answers.
const tree = { name: "site", children: [ { name: "index.html", size: 4 }, { name: "assets", children: [ { name: "logo.svg", size: 2 }, { name: "app.js", size: 6 }, ], }, { name: "notes.txt", size: 1 }, ],}; function countFiles(node) { if (!node.children) return 1; let total = 0; for (const child of node.children) { total = total + countFiles(child); } return total;}- evententerdepth0running total0
Entering site. If it has children, the function will call itself for each one.
That is the real strength of recursion: the code for “one node” also works for the root, for a folder inside the root, and for a folder several levels down. Each call focuses on its own node and trusts the same function to solve the children.
Where you’ll use this
You do not need recursion for every beginner task, but you will meet it in real front-end work whenever data nests. Common examples include:
- Rendering a navigation menu whose items can contain submenus.
- Counting or searching files in a project tree.
- Walking nested comments or replies in a discussion interface.
- Flattening nested arrays after a data import.
- Splitting a problem in half, such as binary search or divide-and- conquer sorting, when you learn algorithms later.
Put the base case near the top, use a smaller input in the recursive case, and test the smallest input first. That one habit prevents a surprising number of recursion bugs.
Common misconceptions
“A recursive function runs all copies at the same time.”
Only the top stack frame is actively running. Older calls are paused, waiting for the call above them to return.
“The base case must always be 0.”
The base case is whatever input has a direct answer. Factorial in this lesson uses 1; summing down often uses 0; a file node uses “no children.”
“Recursion is always more advanced and better.”
Recursion is a tool, not a badge. For flat repetition, a loop is usually easier to read, faster, and stack-safe.
“If I add a base case anywhere, I’m safe.”
The recursive case must actually move toward that base case. If countDown(n) calls countDown(n + 1), a base case at 0 will never be reached from positive numbers.
“JavaScript will optimize tail recursion for me.”
Do not rely on that. Proper tail calls exist in the standard and Safari’s JavaScriptCore implements them, but most JavaScript engines you meet in everyday development do not remove those stack frames.
Practice recursion
5 EXERCISESTrace the values before checking the answer. For the writing tasks, paste the solution into your browser console or editor and add your own extra tests.
Predict the output before running the code.
function factorial(n) {
if (n === 1) return 1;
return n * factorial(n - 1);
}
console.log(factorial(5));The base case returns 1. Then the waiting calls multiply back up: 2, 6, 24, 120. So factorial(5) prints 120.
Write a function that returns n + (n - 1) + ... + 1. Check that sumTo(5) prints 15.
function sumTo(n) {
// your base case and recursive case here
}
console.log(sumTo(5));function sumTo(n) {
if (n === 0) return 0;
return n + sumTo(n - 1);
}
console.log(sumTo(5));The base case handles 0. Every other call moves toward 0 by subtracting 1, then adds the returned smaller sum.
The starter code never stops. Add a base case so countDown(3) eventually prints done.
function countDown(n) {
return countDown(n - 1);
}
console.log(countDown(3));function countDown(n) {
if (n === 0) return "done";
return countDown(n - 1);
}
console.log(countDown(3));The fixed function stops at 0 and returns done. Without that line, it keeps calling itself with smaller and smaller numbers until the stack overflows.
Finish countFiles for the tree from the lesson. It should count files, not folders.
const tree = {
name: "site",
children: [
{ name: "index.html", size: 4 },
{ name: "assets", children: [
{ name: "logo.svg", size: 2 },
{ name: "app.js", size: 6 },
] },
{ name: "notes.txt", size: 1 },
],
};
function countFiles(node) {
// return 1 for a file; otherwise count every child
}
console.log(countFiles(tree));const tree = {
name: "site",
children: [
{ name: "index.html", size: 4 },
{
name: "assets",
children: [
{ name: "logo.svg", size: 2 },
{ name: "app.js", size: 6 },
],
},
{ name: "notes.txt", size: 1 },
],
};
function countFiles(node) {
if (!node.children) return 1;
let total = 0;
for (const child of node.children) {
total = total + countFiles(child);
}
return total;
}
console.log(countFiles(tree));The root has three file leaves: index.html, logo.svg, app.js, and notes.txt, so the answer is 4. The same function handles every folder level.
Rewrite a recursive countdown as a loop that prints exactly 3, 2, 1, Go!.
let n = 3;
const steps = [];
while (n > 0) {
steps.push(n);
n = n - 1;
}
steps.push("Go!");
console.log(steps.join(", "));The loop version keeps one stack frame. It changes n from 3 to 2 to 1 to 0, then prints the same countdown text.
Quiz: check your understanding
8 QUESTIONSEvery answer explains why it is right or wrong. If a code question feels hard, trace the base case and the recursive case on paper.
Question 1 of 8What is recursion?
Choose an answer to see the explanation.
Question 2 of 8What does this print?
Read the code, then predictfunction factorial(n) { if (n === 1) return 1; return n * factorial(n - 1); } console.log(factorial(4));Choose an answer to see the explanation.
Question 3 of 8Which line is the base case?
Read the code, then predictfunction sumTo(n) { if (n === 0) return 0; return n + sumTo(n - 1); } console.log(sumTo(3));Choose an answer to see the explanation.
Question 4 of 8What happens to the call stack while factorial(4) is looking for factorial(1)?
Choose an answer to see the explanation.
Question 5 of 8What error do most browsers show for runaway recursion?
Choose an answer to see the explanation.
Question 6 of 8Which task is usually a better fit for recursion than a loop?
Choose an answer to see the explanation.
Question 7 of 8What do the recursive and loop versions print?
Read the code, then predictfunction sumToRecursive(n) { if (n === 0) return 0; return n + sumToRecursive(n - 1); } function sumToLoop(n) { let total = 0; for (let current = n; current > 0; current = current - 1) { total = total + current; } return total; } console.log(sumToRecursive(4) + "," + sumToLoop(4));Choose an answer to see the explanation.
Question 8 of 8JavaScript engines generally do not optimize ordinary tail recursion. Which wording is safest?
Choose an answer to see the explanation.
Key takeaways
- Recursion means a function calls itself with a smaller version of the same problem.
- Every recursive function needs a reachable base case and a recursive case that moves toward it.
- The call stack grows while calls wait, then shrinks as each call returns.
- Missing or unreachable base cases can throw
RangeErrorwhen the stack limit is reached. - Loops are usually better for flat repetition; recursion is often clearer for trees and other nested data.
Remember the one-liner.
A recursive function answers the smallest case directly, then solves bigger cases by calling itself on smaller ones.
Up next: Objects & arrays in Stage 3 (text only for now), where the tree-shaped data from this lesson gets its full explanation.