The Base Case Stops Recursion — Off-by-One Errors Crash or Skip Results
Designing Base Cases
Carefully chosen base cases prevent infinite recursion and subtle off-by-one bugs whether your input is an array, a string, or a tree node.
What you'll learn
- Identify base cases for recursive functions operating on arrays, strings, and trees
- Avoid off-by-one pitfalls by testing boundary values before writing the recursive case
- Explain why both "empty input" and "single-element input" sometimes need separate base cases
A base case is the answer you can give without making another recursive call. Designing it correctly is the single biggest source of bugs in recursive code: miss it and you get a stack overflow; place it one step too late and you return the wrong answer.
Arrays and Strings
For array or string inputs, the natural base cases are the empty input and, occasionally, the single-element input:
// Sum every element of an array
function sum(arr, i = 0) {
if (i === arr.length) return 0; // base case: past the last index
return arr[i] + sum(arr, i + 1);
}
console.log(sum([1, 2, 3, 4])); // 10
console.log(sum([])); // 0 A common off-by-one mistake is using i > arr.length instead of
i === arr.length, or comparing against arr.length - 1 and then accidentally
skipping the last element. Always test your base case manually with an empty
array and a single-element array before running larger inputs.
Strings
String recursion typically slices off one character per call:
function reverse(s) {
if (s.length <= 1) return s; // base case: empty or single char
return reverse(s.slice(1)) + s[0];
}
console.log(reverse("hello")); // "olleh"
console.log(reverse("")); // ""
console.log(reverse("a")); // "a" Note that s.length <= 1 handles both the empty string and the single-character
string in one guard. Using s.length === 0 alone would cause reverse("a") to
call reverse("") unnecessarily — harmless here, but a symptom of imprecise
thinking.
Trees
Tree nodes typically have null or undefined children at the leaves, making
null the natural base case:
// Count nodes in a binary tree
function countNodes(node) {
if (node === null) return 0; // base case: empty subtree
return 1 + countNodes(node.left) + countNodes(node.right);
} For trees, always ask: can a node’s child ever be undefined vs null? Pick
one convention and guard for both, or use node == null (loose equality) to
catch either.
Two Base Cases Can Be Necessary
Some problems require more than one base case. Binary search is a textbook example:
function binarySearch(arr, target, lo = 0, hi = arr.length - 1) {
if (lo > hi) return -1; // base case 1: not found
const mid = (lo + hi) >> 1;
if (arr[mid] === target) return mid; // base case 2: found
if (arr[mid] < target) return binarySearch(arr, target, mid + 1, hi);
return binarySearch(arr, target, lo, mid - 1);
} Checklist Before Writing the Recursive Case
- What is the smallest legal input? Does the function return the correct value for it?
- What happens if the input is empty, null, or zero?
- Is the recursive call guaranteed to shrink the input toward the base case?
Answering these three questions catches most base-case bugs before any code is written.
Up Next
Even correct base cases do not help if every recursive call leaves a pending frame on the stack. Learn about tail recursion — and how to work around V8’s lack of tail-call optimization.
Tail Recursion →