Debugging

15 canonical bug patterns. Click any to see the failing test and the fix.

def fib(n):
    if n == 0:
        return 0
    if n == 1:
        return 0
    return fib(n - 1) + fib(n - 2)
Failing test
Input: fib(6)
Expected: 8
Actual: 0
The bug

Base case for n == 1 returns 0 but should return 1.

fib(1) must return 1, not 0; with both base cases returning 0 every recursive sum is 0 + 0. Memoization is a performance concern, not a correctness one, so it can't be the cause of the wrong value.

function printIndices(arr) {
  const out = [];
  for (var i = 0; i < arr.length; i++) {
    setTimeout(() => out.push(i), 0);
  }
  return new Promise((resolve) => {
    setTimeout(() => resolve(out), 10);
  });
}
Failing test
Input: await printIndices(['a', 'b', 'c'])
Expected: [0, 1, 2]
Actual: [3, 3, 3]
The bug

var i is function-scoped, so every closure reads the final post-loop value of i.

Because var hoists i to function scope, all three callbacks share the same binding and read 3 after the loop completes; switching to let creates a per-iteration binding. Timer ordering is fine here — they run in insertion order, just all after the loop ends.

public static int sumAll(int[] arr) {
    int sum = 0;
    for (int i = 0; i <= arr.length; i++) {
        sum += arr[i];
    }
    return sum;
}
Failing test
Input: sumAll(new int[]{1, 2, 3})
Expected: 6
Actual: ArrayIndexOutOfBoundsException
The bug

The loop condition uses <= instead of <, reading one past the last index.

Valid indices are 0..length-1, so i <= arr.length dereferences arr[3] on a length-3 array and throws. Initializing to 0 is correct; the bug is the bound, not the seed.

def collect(x, bucket=[]):
    bucket.append(x)
    return bucket

def run():
    a = collect(1)
    b = collect(2)
    return (a, b)
Failing test
Input: run()
Expected: ([1], [2])
Actual: ([1, 2], [1, 2])
The bug

The default [] is evaluated once at definition time and shared across all calls.

Default arguments are evaluated once when the def runs, so every call without an explicit bucket reuses the same list object. append does return None, but bucket is never reassigned from its return value, so that's a red herring.

public static int midpoint(int low, int high) {
    int mid = (low + high) / 2;
    return mid;
}
Failing test
Input: midpoint(2_000_000_000, 2_000_000_000)
Expected: 2000000000
Actual: -147483648
The bug

low + high overflows int before the division, wrapping to a negative value.

Adding two values near Integer.MAX_VALUE overflows int (max ~2.147e9), wrapping into the negative range before the divide happens. Use low + (high - low) / 2 or cast to long. Operator precedence is fine: + and / behave as expected with explicit parentheses.

def drop_evens(nums):
    for n in nums:
        if n % 2 == 0:
            nums.remove(n)
    return nums
Failing test
Input: drop_evens([1, 2, 4, 5, 6])
Expected: [1, 5]
Actual: [1, 4, 5]
The bug

Mutating nums during iteration causes the iterator to skip the element after each removal.

The list iterator advances by index; when index 1 (value 2) is removed, 4 slides into index 1 and the next iteration jumps to index 2, skipping it. Build a new list with a comprehension instead. remove does delete by value, so option a is incorrect.

function sortNumbers(arr) {
  return arr.sort();
}
Failing test
Input: sortNumbers([10, 2, 33, 4])
Expected: [2, 4, 10, 33]
Actual: [10, 2, 33, 4]
The bug

sort with no comparator coerces elements to strings, producing lexicographic order.

Default Array.prototype.sort converts each element to a string and compares UTF-16 code units, so '10' < '2'. Pass (a, b) => a - b for numeric order. sort mutates and returns the same array, so option a is wrong.

def find_max(nums):
    m = 0
    for n in nums:
        if n > m:
            m = n
    return m
Failing test
Input: find_max([-3, -1, -7, -4])
Expected: -1
Actual: 0
The bug

Initializing m to 0 means no negative input can ever exceed it.

With all-negative input every n > 0 is false, so m stays at 0 and is returned. Seed with nums[0] (after an empty-list check) or float('-inf'). The strict > vs >= choice doesn't affect the final max, only which equal element is kept.

public static String shout(String s) {
    s.replace("l", "L");
    return s;
}
Failing test
Input: shout("hello")
Expected: heLLo
Actual: hello
The bug

String is immutable; replace returns a new String that is discarded.

String objects can't be modified in place; replace returns a brand-new String that the code throws away. Reassign with s = s.replace(...). String.replace already replaces every occurrence (it's replaceFirst that doesn't), and it does not use regex.

function keepPositive(arr) {
  arr.filter((x) => x > 0);
  return arr;
}
Failing test
Input: keepPositive([-1, 2, -3, 4])
Expected: [2, 4]
Actual: [-1, 2, -3, 4]
The bug

filter is non-mutating; its returned array is discarded, so arr is unchanged.

Array.prototype.filter returns a new array and does not modify the original; the caller has to use its return value (return arr.filter(...)). The predicate behaves correctly — negatives are filtered out of the returned array, which is just thrown away.

def avg(nums):
    return sum(nums) / len(nums)
Failing test
Input: avg([])
Expected: 0
Actual: ZeroDivisionError
The bug

Empty input isn't handled, so len(nums) is 0 and triggers division by zero.

An empty list yields sum([]) = 0 and len([]) = 0, and 0 / 0 raises ZeroDivisionError. Guard with if not nums: return 0 (or raise) before dividing. sum([]) is 0, not None, and / is true division in Python 3.

public static String greet(String name) {
    String trimmed = name.trim();
    if (trimmed == "admin") {
        return "hi boss";
    }
    return "hi " + trimmed;
}
Failing test
Input: greet(" admin ")
Expected: hi boss
Actual: hi admin
The bug

== compares references; trimmed is a new String object, not interned, so it isn't == "admin".

trim() returns a fresh String that isn't the same object as the interned literal "admin", so == is false; use .equals("admin"). trim removes whitespace from both ends, and literals are immutable.

def reverse(s):
    out = ""
    for i in range(len(s)):
        out = s[i] + out
        s = s[1:]
    return out
Failing test
Input: reverse("abcd")
Expected: dcba
Actual: ca
The bug

Slicing s inside the loop shrinks the string while i keeps advancing, skipping characters.

len(s) is captured once when range is called, but s shrinks every iteration, so s[i] indexes into a moving target and the loop both skips characters and risks IndexError on longer inputs. Either drop the s = s[1:] line or iterate with reversed(s). Concatenation order is actually correct for prepend-based reversal.

function isEven(n) {
  return n % 2 == 0;
}
Failing test
Input: isEven(0.5)
Expected: false
Actual: false
The bug

The result is correct, but the function silently accepts non-integers it shouldn't — there's no input guard for floats.

0.5 % 2 is 0.5, which isn't == 0, so the function returns false — which happens to match expected, but only by accident. The real bug is that isEven has no precondition on integer input; callers like isEven(2.0000000001) will get surprising results. % on floats returns the IEEE remainder, not NaN.

public static String grade(int score) {
    switch (score / 10) {
        case 10:
        case 9:
            return "A";
        case 8:
            return "B";
        case 7:
            "C";
        case 6:
            return "D";
        default:
            return "F";
    }
}
Failing test
Input: grade(75)
Expected: C
Actual: D
The bug

case 7 has a bare string expression instead of a return, so execution falls through to case 6.

The bare expression "C"; is a valid statement but doesn't return, so without a break Java falls through into case 6 and returns "D". Stacked case labels without code between them are legal and exit via the first reachable return.