Skip to content
beginner

Recursion in JavaScript: Base Cases and Step-by-Step Tracing

That is the normal first experience with recursion. The function calls itself, the answer appears, and somewhere in the middle your mental model goes dark.…

Published 2026-10-03Updated 2026-10-049 min read
Idyllic white sand beach and azure ocean under clear skies in Zanzibar, Tanzania.
Idyllic white sand beach and azure ocean under clear skies in Zanzibar, Tanzania. Photo by Ana Kenk on Pexels.

You run the code. It prints the right number. And you still cannot explain why.

That is the normal first experience with recursion. The function calls itself, the answer appears, and somewhere in the middle your mental model goes dark. This article fixes that by slowing the whole thing down: find the base case, watch the calls pile up, watch the values come back down.

You already know functions, if/else, and loops, so you have everything you need. If any of those feel shaky, a quick review of function parameters and return values will make this much easier.

What Makes a Function Recursive

A recursive function is a function that calls itself by name inside its own body. That is the whole definition. No magic, no special syntax.

Every recursive function needs exactly two parts:

  • A base case: a condition that returns a value without calling the function again.
  • A recursive step: a call to the function with a smaller or simpler input.

The recursive step must move toward the base case. If the input never gets smaller, the calls never stop.

If you have written a while loop, you already understand the shape of this. A loop repeats code until a condition turns false. Recursion repeats a function call until a condition turns true. Same goal, different machinery.

Here is the mental model that makes everything else click: each call pauses and waits. When a function calls itself, the current call does not finish. It stops at that line and waits for the next call to hand back a value. Those paused calls stack up like sticky notes on a desk, and they only get resolved in reverse order.

Knowledge check

Check your understanding

Answer this question before you continue.

Which description correctly identifies the two parts every recursive function needs?
Single Choice

Focus: Identify the two required parts of a recursive function and their roles.

Your First Recursive Function: Countdown

Start with something small enough to hold in your head.

function countdown(n) {
  if (n === 0) {
    console.log("Done!");
    return;
  }
  console.log(n);
  countdown(n - 1);
}

countdown(3);
3
2
1
Done!

Two lines matter here. The if (n === 0) block is the base case — it stops the calls. The countdown(n - 1) line is the recursive step — it calls the function again with a smaller number.

Notice that the output appears in the same order you would get from a loop. That is because this particular function does its work before the recursive call. The interesting part comes later, when a function does work after the call returns.

Knowledge check

Check your understanding

Answer this question before you continue.

What does this call print, in order?
Output Prediction

Focus: Trace the output order of a recursive countdown that logs before making its recursive call.

function countdown(n) {
  if (n === 0) {
    console.log("Done!");
    return;
  }
  console.log(n);
  countdown(n - 1);
}

countdown(2);

The Base Case: The Brake, Not the Boring Part

Beginners tend to treat the base case as a formality. It is actually your first diagnostic tool. If you cannot point at the line that stops the calls, the function is not finished.

Remove the base case and see what happens:

function countdown(n) {
  console.log(n);
  countdown(n - 1);
}

countdown(3);
3
2
1
0
-1
-2
... (continues until the engine stops it)

Eventually the engine throws an error like this:

RangeError: Maximum call stack size exceeded

That is a stack overflow. Every paused call takes up memory, and the engine has a limit. When you exceed it, the program crashes. Recognizing this error by sight saves you real debugging time, because it almost always means one of two things:

  • There is no base case at all.
  • There is a base case, but the input never reaches it.

The second one is sneakier. Consider this:

function countdown(n) {
  if (n === 0) {
    return;
  }
  console.log(n);
  countdown(n); // forgot to subtract
}

The base case exists. The condition is correct. But countdown(n) passes the same n forever, so n never becomes 0. The fix is one character: countdown(n - 1).

Common mistake: Writing the base case but forgetting to change the argument in the recursive call. The function looks correct at a glance and crashes at runtime.

A good beginner instinct: after writing any recursive function, ask yourself, "What value does this argument have on the next call, and is it closer to the base case?" If you cannot answer in one sentence, the function is not ready.

Knowledge check

Check your understanding

Answer this question before you continue.

This function has a base case but still fails to reach it when called with 3. What change fixes the cause?
Debugging

Focus: Diagnose a recursive call that fails to make progress toward its base case.

function countdown(n) {
  if (n === 0) {
    return;
  }
  countdown(n);
}

Tracing a Recursive Call Step by Step

Four sumTo calls descend from sumTo(4) to the base case sumTo(1). Return values travel back upward: 1, 3, 6, then 10, completing the waiting additions.
Each call waits on the way down; the result is assembled in reverse order on the way back up.

Now the real skill. Tracing means following the calls by hand, one at a time, until you can predict the output before running it.

Here is a small function that adds up all the numbers from 1 to n:

function sumTo(n) {
  if (n === 1) {
    return 1;
  }
  return n + sumTo(n - 1);
}

console.log(sumTo(4));
10

Why 10? Trace it. Write one line per call and mark which call is currently waiting.

The downward phase — each call checks its base case and prepares the next argument, but the addition itself is left pending:

  1. sumTo(4) checks 4 === 1 → false. It needs 4 + sumTo(3). Waiting.
  2. sumTo(3) checks 3 === 1 → false. It needs 3 + sumTo(2). Waiting.
  3. sumTo(2) checks 2 === 1 → false. It needs 2 + sumTo(1). Waiting.
  4. sumTo(1) checks 1 === 1 → true. It returns 1 immediately. No waiting.

The upward phase — the base case returns first, then each paused call finishes:

CallWaiting forReceivesReturns
sumTo(1)nothing—1
sumTo(2)sumTo(1)12 + 1 = 3
sumTo(3)sumTo(2)33 + 3 = 6
sumTo(4)sumTo(3)64 + 6 = 10

The answer is assembled on the way back up, not on the way down. This is the part beginners miss. Each call on the way down still does real work: it checks the base case and computes the next argument. What gets deferred is the addition that sits around the recursive call. That arithmetic happens in reverse order as the values return.

Tip: When tracing on paper, draw an arrow down for each new call and an arrow back up for each return value. If you can fill in the "Returns" column without running the code, you understand the function.

Knowledge check

Check your understanding

Answer this question before you continue.

What value does `sumTo(3)` return with this definition?
Output Prediction

Focus: Trace the return values of a recursive sum function through its base case and waiting calls.

function sumTo(n) {
  if (n === 1) {
    return 1;
  }
  return n + sumTo(n - 1);
}

Recursion vs a Loop: Which One Should You Write?

For flat, countable work — summing numbers, walking an array, counting down — a for or while loop is usually shorter, faster, and easier to debug. I reach for a loop by default.

Recursion earns its place when the data itself is nested: folders inside folders, comments inside comments, DOM nodes inside DOM nodes. The shape of the data decides the tool.

LoopRecursion
Use this whenThe data is flat: a list, a range, a countThe data is nested or tree-shaped
ReadabilityShorter for simple repetitionClearer when the structure repeats itself
Memory costOne set of variables, reusedOne paused call per level of depth
Beginner mistakeOff-by-one in the conditionMissing or unreachable base case

The honest tradeoff: every recursive call uses stack space, so deep recursion can crash where a loop would keep going. If you are processing a list of ten thousand items, a loop is the safer choice. If you are walking a tree that is five levels deep, recursion is the readable choice.

A simple rule: choose the loop when the shape of the data is flat; choose recursion when the shape of the data is recursive.

Where Recursion Shows Up in Real Code

You will meet recursion again in practical work, usually without anyone calling it that.

Walking a nested tree structure is the classic case. A file system is folders containing folders containing files. A comment thread is comments containing replies containing replies. A DOM node is an element containing child elements. In each case, the data has unknown depth, and a recursive function handles that naturally.

Processing nested data of unknown depth is the second case. If you receive a JSON object where values can themselves be objects, a recursive function can visit every level without you knowing the depth in advance.

Later, when you meet searching and sorting algorithms, you will find that the recursive versions are often the readable ones. That is a topic for another day — for now, the goal is recognizing the shape.

Practice: Trace It, Then Rewrite It

Two short tasks. Do them on paper before you run anything.

Task 1: Trace this function.

function mystery(n) {
  if (n === 0) {
    return 0;
  }
  return n + mystery(n - 1);
}

console.log(mystery(3));

Write out the call order and the value each call returns. Then run it and compare.

Hint: Build a table like the one in the tracing section. The downward phase has four calls; the upward phase has four returns.

What a correct trace looks like: mystery(3) waits for mystery(2), which waits for mystery(1), which waits for mystery(0). The base case returns 0. Then mystery(1) returns 1, mystery(2) returns 3, and mystery(3) returns 6.

Task 2: Rewrite it as a loop.

Take the sumTo function from earlier and rewrite it using a for loop. Then compare the two versions side by side.

Hint: Start with a variable set to 0, loop from 1 to n, and add each number.

What to notice: The loop version is shorter and does not pause anything. The recursive version mirrors the mathematical definition more directly. Neither is wrong — but for this flat, countable task, the loop is the version I would ship.

Optional extension: Take the broken countdown(n) from the base case section, add the missing - 1, and watch the stack overflow error disappear.

What to Carry Forward

When you meet a recursive function in the wild, do three things in order. Find the base case. Confirm the input shrinks toward it. Trace the calls on paper before trusting the output.

Recursion feels slippery for the first few attempts. That is not a sign you are bad at it — it is a sign that you are holding several paused calls in your head at once, which is genuinely new work. Tracing small examples is the normal way it clicks, and it clicks faster than you expect.

Your next practical step: take an array of objects — say, a list of categories where each category can contain subcategories — and write a function that visits every item at every level. That is where recursion stops being an exercise and starts being the obvious tool.

Knowledge check

Final check

Finish the article by checking the ideas you just learned.

A task must process a flat list of ten thousand items. Which approach best follows the article's guidance?
Question 1 of 2Misconception Check

Focus: Choose between a loop and recursion based on whether the work is flat or nested.

You need to visit every element in a structure where each folder can contain more folders, with depth unknown in advance. Which choice best matches the article's guidance?
Question 2 of 2Single Choice

Focus: Recognize when recursion naturally fits nested data of unknown depth.

References

  1. Functions - JavaScript | MDNdeveloper.mozilla.org
Practical resource

Want a more structured JavaScript path?

Use the JavaScript for AI Applications Starter Pack to turn individual tutorials into a focused path from language fundamentals to interactive AI applications.

View the bundle
Coming soon

JavaScript for AI Applications Starter Pack

Build the JavaScript foundation behind modern interactive applications. The JavaScript for AI Applications Starter Pack takes you from core language concepts through functions, browser events, forms, DOM updates, application state, debugging, and practical projects—so you can learn how interfaces take input, work with structured data, respond to users, and turn results into experiences people can actually use.

$9
PDF BundleJavaScriptWeb DevelopmentAI Applications
  • 253-page illustrated PDF
  • 12 guided JavaScript chapters
  • Visual concept diagrams
  • Self-assessment quizzes
  • Bonus deep-dive sections
  • DOM, events, forms & application state
  • Browser projects, debugging & practical workflows

Coming soon

Keep learning

Related tutorials

Continue with nearby JavaScript topics and beginner-friendly explanations.