Real-world examples of faulty recursion logic (and how to fix them)
When people search for examples of examples of faulty recursion logic, they’re usually not looking for textbook Fibonacci. They want the nasty, realistic cases that slip past code review. Let’s start there and work our way into patterns and fixes.
We’ll walk through several examples of faulty recursion logic that show up in everyday code:
- A missing base case that only fails on certain inputs
- A base case that can never be reached
- A recursive step that moves in the wrong direction
- Recursion mixed with mutation that corrupts shared state
- Misused memoization that explodes memory instead of saving time
- Infinite recursion via mutual calls between two functions
Each example of recursion failure here is intentionally realistic: something you could plausibly see in a pull request, not just in a lecture slide.
Example of a missing base case: naive string reverse in Python
Here’s a deceptively simple example of faulty recursion logic: reversing a string.
## Buggy version
def reverse_string(s: str) -> str:
# # Intention: stop when the string is empty
if s == "":
return s
# # Bug: slicing doesn't reduce input for some edge cases
return reverse_string(s[1:]) + s[0]
At first glance, this looks fine. For normal strings, s[1:] shrinks the input until you hit the empty string. But consider this call:
reverse_string("\0") # string with a single null character
That still works. The real danger is when this pattern gets copied into code that operates on linked lists, trees, or custom sequence types where s[1:] (or the equivalent) does not actually create a smaller problem. You end up with recursion that never converges.
The pattern to remember from this example of a recursion bug:
- The base case looks correct on its own.
- The recursive call looks correct on its own.
- Together, they don’t guarantee progress toward the base case for all valid inputs.
A safer mindset in 2024: when you write recursive code, treat “input is strictly smaller” as a contract, and assert it in tests. Property‑based testing libraries like Hypothesis (Python, .org) can help generate weird edge cases that break naive recursion.
Tree traversal gone wrong: best examples of subtle infinite recursion
One of the best examples of faulty recursion logic is a tree traversal that accidentally walks in circles. You see this in graph‑shaped data where someone assumes “tree” but actually has a general graph with cycles.
// Buggy depth-first traversal
function printNodes(node) {
if (!node) return;
console.log(node.value);
// Assumes children form a tree with no cycles
for (const child of node.children) {
printNodes(child); // can revisit ancestors and loop forever
}
}
This looks like a textbook example of recursion. The hidden bug: if node.children ever contains a reference back to an ancestor (or to the same node), you get infinite recursion and a stack overflow.
Improved version with a visited set:
function printNodes(node, visited = new Set()) {
if (!node || visited.has(node)) return;
visited.add(node);
console.log(node.value);
for (const child of node.children) {
printNodes(child, visited);
}
}
This is one of the most common examples of faulty recursion logic in production: code written as if the world were a tree, then deployed into a graph. Static analyzers and code review tools have gotten better in 2024 at warning about this pattern, but they’re not perfect. Treat any recursive traversal of user‑generated or database‑loaded relationships as suspect until you prove there are no cycles.
Off-by-one in base cases: factorial that lies to you
Another classic example of faulty recursion logic is the off‑by‑one base case. The function terminates, but returns the wrong answer for a whole class of inputs.
// Buggy factorial
public static long factorial(int n) {
if (n == 0) {
return 0; // should be 1
}
return n * factorial(n - 1);
}
This won’t crash. It will just silently return 0 for any n > 0, because the recursion eventually multiplies by 0.
Why this matters in 2024–2025: a lot of AI‑generated code and auto‑completion suggestions reproduce this example of a faulty recursion logic pattern. Developers skim, see “oh yeah, factorial,” and move on. The code ships. The bug lurks.
Good tests catch this quickly. A minimal fix:
if (n == 0) {
return 1;
}
The lesson: when you see a recursive definition that mirrors a math formula, double‑check that the base case matches the mathematical identity. It’s boring, and it saves you.
Mutual recursion: two functions calling each other forever
Not all examples of faulty recursion logic are self‑calls. Sometimes two functions politely bounce the problem back and forth until the stack blows.
// Buggy mutual recursion
function isEven(n: number): boolean {
if (n === 0) return true;
return isOdd(n); // should be isOdd(n - 1)
}
function isOdd(n: number): boolean {
if (n === 0) return false;
return isEven(n); // should be isEven(n - 1)
}
For any non‑zero n, this pair never moves toward a base case. It’s a clean, minimal example of faulty recursion logic where the problem is not the base case itself, but the missing progress step.
Fixed version:
function isEven(n: number): boolean {
if (n === 0) return true;
return isOdd(n - 1);
}
function isOdd(n: number): boolean {
if (n === 0) return false;
return isEven(n - 1);
}
Linters in modern TypeScript/JavaScript environments can sometimes flag mutually recursive functions that lack an obvious decreasing argument, but they’re far from perfect. When you see mutual recursion, ask one question: where does the argument shrink?
Sorting with recursion: quicksort that never partitions
Sorting algorithms are a rich source of real examples of faulty recursion logic because they combine recursion with index arithmetic.
Here’s a broken quicksort in Python:
## Buggy quicksort
def quicksort(arr, low, high):
if low >= high:
return
pivot = arr[high]
i = low
for j in range(low, high):
if arr[j] <= pivot:
arr[i], arr[j] = arr[j], arr[i]
i += 1
arr[i], arr[high] = arr[high], arr[i]
# # Bug: recursive calls reuse the same bounds
quicksort(arr, low, high)
quicksort(arr, low, high)
Two issues:
- The sub‑array bounds never change.
- The recursion never shrinks the problem.
Result: infinite recursion until the interpreter or runtime stops you.
Corrected version:
quicksort(arr, low, i - 1)
quicksort(arr, i + 1, high)
This is one of the best examples of how a single typo in recursive parameters can turn a correct algorithm into a stack‑overflowing mess. Code review tip: for any recursive call, read the argument list out loud and explain how each argument moves toward the base case.
Shared mutable state: recursion that corrupts its own inputs
Not all recursion bugs are about infinite loops. Some examples of faulty recursion logic return the wrong answer because of hidden shared state.
Consider a function that collects all paths in a grid:
## Buggy version using shared list
def find_paths(grid, x, y, path, all_paths):
if x == len(grid) - 1 and y == len(grid[0]) - 1:
all_paths.append(path)
return
if x + 1 < len(grid):
path.append("D")
find_paths(grid, x + 1, y, path, all_paths)
path.pop()
if y + 1 < len(grid[0]):
path.append("R")
find_paths(grid, x, y + 1, path, all_paths)
path.pop()
The intention is to backtrack correctly, but a common variant of this pattern forgets to pop() or reuses path without copying it in the base case:
## Subtle bug: append the same list reference
all_paths.append(path) # instead of all_paths.append(list(path))
Now every entry in all_paths points to the same list object, which ends up empty or incorrect by the time recursion finishes. The recursion terminates, but the result is garbage.
This is a textbook example of faulty recursion logic where the function’s interface looks fine, but the interaction between recursion and mutable data structures is wrong.
Memoization mistakes: caching the wrong thing
Memoization is often recommended in 2024–2025 for performance, especially in languages like Python and JavaScript where recursion is expensive. But it introduces its own class of examples of faulty recursion logic.
Buggy Fibonacci with memoization:
## Buggy memoized Fibonacci
def fib(n, memo={}):
if n in memo:
return memo[n]
if n <= 1:
return n
result = fib(n - 1) + fib(n - 2)
memo[n] = result
return result
Two problems:
memois a mutable default argument shared across calls.- The recursive calls
fib(n - 1)andfib(n - 2)don’t passmemo, so they use a different cache.
This is an example of faulty recursion logic where the function looks memoized but behaves like the naive exponential version for each top‑level call, and also leaks state between unrelated calls.
Fixed version:
def fib(n, memo=None):
if memo is None:
memo = {}
if n in memo:
return memo[n]
if n <= 1:
return n
result = fib(n - 1, memo) + fib(n - 2, memo)
memo[n] = result
return result
Static analysis tools and linters (for example, flake8-bugbear in Python) increasingly warn about mutable default arguments, but they can’t always see the logical intent of memoization. You still need to reason carefully about how each recursive call shares or isolates state.
Modern debugging habits for recursion (2024–2025)
Studying examples of examples of faulty recursion logic is only half the story. The other half is building habits and using tools that make these bugs easier to catch.
Some practical approaches:
- Limit recursion depth in tests. Many platforms let you configure stack size; combine that with small, randomized inputs to catch runaway recursion early.
- Use assertions on arguments. Assert that arguments move monotonically toward the base case (for example,
assert n >= 0andassert next_n < n). - Leverage static analyzers. Tools like mypy (Python), ESLint/TypeScript, and Java analyzers can flag missing returns, suspicious mutual recursion, or unbounded recursion patterns.
- Instrument recursion. In performance‑sensitive or safety‑critical code (think medical devices or scientific computing), count recursive calls and log when they exceed a threshold.
For context on how software bugs can affect safety‑critical systems, it’s worth reading general software reliability discussions from organizations like NIST and academic software engineering programs such as MIT OpenCourseWare. While they’re not about recursion only, they reinforce why these “small” logical errors matter.
FAQ: short answers about recursion bugs
What are some common examples of faulty recursion logic?
Common examples of faulty recursion logic include missing or unreachable base cases, recursive calls that don’t move inputs toward the base case, mutual recursion without a decreasing argument, and misuse of shared mutable state or memoization that corrupts results.
Can you give an example of recursion that passes tests but is still wrong?
Yes. The factorial example of returning 0 for n == 0 instead of 1 often passes shallow tests like factorial(1) or factorial(2). Only when you test higher values—or compare against a known correct implementation—does the bug reveal itself.
How do I debug infinite recursion in practice?
Reproduce the issue with the smallest input that triggers it, then add logging or a debugger breakpoint at the start of the recursive function. Inspect the arguments to confirm whether they’re getting closer to the base case. If not, you’ve found an example of faulty recursion logic: either the base case is wrong, or the recursive step doesn’t shrink the problem correctly.
Are recursive functions always a bad idea for production code?
No. Recursion is often perfectly fine, especially for naturally recursive problems like tree traversals or parsing. The key is to understand the limits of your runtime’s call stack, test thoroughly, and watch for the patterns you’ve seen in these real examples of faulty recursion logic.
How can I avoid these kinds of recursion bugs?
Write tests that hit edge cases, reason explicitly about how each call moves toward termination, and consider iterative implementations when recursion becomes hard to reason about. Studying concrete examples of faulty recursion logic—like the ones in this article—sharpens your instincts so you recognize dangerous patterns before they hit production.
Related Topics
Explore More Logical Errors
Discover more examples and insights in this category.
View All Logical Errors