JavaScript recursion stuck? Trace the work left before changing the code
Your recursive function has a stopping condition. It still ends with a call-stack error. Before changing the condition or rewriting the whole function, check what the next call receives. A stopping rule only helps if the work can reach it.
Recursion means a function calls itself to solve part of a problem. It can be useful for nested structures, but you can learn its mechanics with a tiny list. You need a way to finish, progress towards that finish and a clear meaning for the answer returned.
This guide uses an original, illustrative JavaScript example and a small trace sheet. It assumes you can read a function and an if statement. The focus is explaining each call before trying a fix.
Check three things before changing the code
1. What is the stopping point?
Purpose: Identify the case that needs no further call.
The base case is the situation where the function can return an answer directly. For a count, no items left means the answer is zero. Write that meaning in a sentence before inspecting the condition.
Check that the base case returns before the recursive call. A condition can appear in the function without actually preventing another call. Read the order of the statements as well as the comparison.
2. What work remains?
Purpose: Show how each call moves towards the stopping point.
Choose a measure: the number of unvisited items, the remaining distance or another quantity that describes the unfinished work. Compare its value before and after the next call.
The parameter itself does not always become smaller. An index, the numbered position of an item in an array, can increase while the number of items left decreases. Follow the work remaining, rather than relying on the direction of one number.
3. What comes back?
Purpose: Explain how the caller uses the returned answer.
A recursive call eventually returns to the function that called it. Ask what that caller still needs to do. In a count, it can add one for the current item to the answer for the remaining items.
Reaching a base case explains why the calls finish. Combining their answers correctly explains why the final result is useful. Trace both parts.
A fictional example: counting review cards
Illustrative example: a fictional coding workshop has three review cards labelled Label, Keyboard and Error message. They are stored in an array, an ordered collection of values. JavaScript array positions begin at zero.
For ordinary counting, use cards.length. The deliberately longer function below is a learning exercise that makes the calls and returned answers visible. It does not perform an accessibility review or inspect the contents of a card.
function countReviewCards(cards) {
if (!Array.isArray(cards)) {
throw new TypeError("Pass an array of review cards.");
}
function countFrom(index) {
if (index >= cards.length) {
return 0;
}
return 1 + countFrom(index + 1);
}
return countFrom(0);
}
const reviewCards = ["Label", "Keyboard", "Error message"];
console.log(countReviewCards(reviewCards)); // 3
console.log(countReviewCards([])); // 0
The outer function checks that the supplied value is an array. Array.isArray performs that check. A different value produces a TypeError, an error indicating an unsuitable type, before the counting begins.
The inner helper, countFrom, starts at position zero. It returns zero when the index reaches the array length. Otherwise, it counts one position and asks the next call to count the rest. Use a small, ordinary array that stays unchanged during this synchronous exercise.
Trace the calls, then trace the returns
For the three-card example, the amount of work left is cards.length - index. Fill in the next action before running the snippet in a local JavaScript practice file.
| Index | Work left | Next action |
|---|---|---|
| 0 | 3 | Count one and call with index 1. |
| 1 | 2 | Count one and call with index 2. |
| 2 | 1 | Count one and call with index 3. |
| 3 | 0 | Return zero. Do not call again. |
Now follow the answers back. The call at index 2 receives zero and returns one. Index 1 receives one and returns two. Index 0 receives two and returns three. The empty array reaches the base case immediately and returns zero.
Compare that with the broken line return 1 + countFrom(index);. For a non-empty array, the index stays the same. At the start, the work-left values would be 3, 3, 3. The base case exists, but this path never reaches it. Trace the mistake on paper rather than deliberately running an unbounded example.
Reuse this small trace sheet
For your own recursive function, write these five lines beside the code. If it makes several recursive calls, check every branch, including a branch containing repeated values.
- Input and result: which values are allowed, and what should one call return?
- Base case: which input returns without another call?
- Work left: what measure shows the unfinished work?
- Next calls: what are the first three inputs and their work-left values?
- Return journey: what happens to the answer as each caller resumes?
Try an empty array, one card and repeated labels. Repeated labels still occupy separate positions, so two identical labels should count as two. Also try an invalid input and check that the outer guard rejects it.
Finishing does not guarantee a safe depth
The call stack keeps track of active function calls. A recursive calculation can move towards a valid base case and still exceed the available stack if it needs too many nested calls. There is no single depth limit to promise across JavaScript environments.
For this simple count, the array length is the clearer solution. For other tasks, consider iteration or an explicit work list where appropriate. Keep the distinction clear: progress towards a stopping point and practical limits are separate questions.
Your next ten minutes
Copy the working example, predict the answers for an empty array and two identical labels, then run it. Check the predictions against the output. Afterwards, explain the three-card trace to someone without reading the return expression aloud.
Take the same sheet to the function you are debugging. If you cannot name what changes between calls, that is a useful place to investigate. Keep the input tiny until you can explain the stopping point and the answer that comes back.
This evergreen guide was prompted by a freeCodeCamp learner discussion. The workshop scenario, code and trace sheet were composed for Coded Female. Recursion, tracing and decreasing-work checks are established techniques; technical explanations were checked against the MDN references below.


