DEV Community

Viram Choksi
Viram Choksi

Posted on Originally published at trybughunt.com

The 7 Sneakiest Bugs That Slip Past 100% Test Coverage (And How to Spot Them)

We’ve all heard the myth: "If the tests are green, the code is ready for production."

Then at 11:30 PM on a Friday, an unhandled exception brings down the API.

The truth is, automated tests test the code paths you remembered to write tests for. But real-world code has sneaky language quirks, silent memory mutations, and edge cases that look 100% syntactically innocent until they meet real user data.

Here are 7 of the sneakiest bugs in modern software development—and how to spot the pattern before your users do.


1. The Reference Trap: [[]] * 3 in Python

The Innocent Code:

# Create a 3x3 grid
grid = [[]] * 3
grid[0].append("bug")

print(grid)
Enter fullscreen mode Exit fullscreen mode

What you expect:
[['bug'], [], []]

What actually happens:
[['bug'], ['bug'], ['bug']]

Why it breaks:
In Python, the * operator on a list does not make deep copies. It creates three copies of the pointer to the exact same list in memory. When you mutate grid[0], all three positions reflect the change because they are all referencing the exact same object.

The Fix:
Use a list comprehension to create independent inner lists:

grid = [[] for _ in range(3)]
Enter fullscreen mode Exit fullscreen mode

2. The Iterator Shift: Mutating an Array While Looping

The Innocent Code:

nums = [2, 4, 6, 8]

for n in nums:
    nums.remove(n)

print(nums)
Enter fullscreen mode Exit fullscreen mode

What you expect:
[] (An empty list)

What actually happens:
[4, 8]

Why it breaks:
Python’s loop maintains an internal index pointer.

  • On iteration 0, it removes 2. The list becomes [4, 6, 8].
  • Now, index 0 is 4, and index 1 is 6!
  • The loop advances to iteration 1, which reads index 1 (6), skipping 4 completely!

The Fix:
Iterate over a shallow copy (nums[:]) or use a list comprehension:

nums = [n for n in nums if not should_remove(n)]
Enter fullscreen mode Exit fullscreen mode

3. The parseInt in Array.prototype.map

The Innocent Code:

const numbers = ["10", "10", "10", "10"].map(parseInt);
console.log(numbers);
Enter fullscreen mode Exit fullscreen mode

What you expect:
[10, 10, 10, 10]

What actually happens:
[10, NaN, 2, 3]

Why it breaks:
Array.prototype.map passes three arguments to its callback: (element, index, array).

parseInt takes two arguments: (string, radix).

So JavaScript actually executes:

  • parseInt("10", 0) -> Radix 0 defaults to decimal -> 10
  • parseInt("10", 1) -> Radix 1 is invalid -> NaN
  • parseInt("10", 2) -> Radix 2 (binary 10) -> 2
  • parseInt("10", 3) -> Radix 3 (ternary 10) -> 3

The Fix:
Pass an explicit arrow function:

const numbers = ["10", "10", "10", "10"].map(str => parseInt(str, 10));
// Or simply:
const numbers = ["10", "10", "10", "10"].map(Number);
Enter fullscreen mode Exit fullscreen mode

4. The Mutable Default Argument

The Innocent Code:

def add_user_to_cache(user, cache=[]):
    cache.append(user)
    return cache

print(add_user_to_cache("Alice"))
print(add_user_to_cache("Bob"))
Enter fullscreen mode Exit fullscreen mode

What you expect:
['Alice']

['Bob']

What actually happens:
['Alice']

['Alice', 'Bob']

Why it breaks:
Python evaluates default function arguments once when the function is defined, not every time it is called. That default [] is stored in the function's __defaults__ tuple for the lifetime of the process.

The Fix:
Use None as a sentinel value:

def add_user_to_cache(user, cache=None):
    if cache is None:
        cache = []
    cache.append(user)
    return cache
Enter fullscreen mode Exit fullscreen mode

5. JavaScript's Alphabetical .sort()

The Innocent Code:

const scores = [1, 25, 5, 100, 12];
scores.sort();
console.log(scores);
Enter fullscreen mode Exit fullscreen mode

What you expect:
[1, 5, 12, 25, 100]

What actually happens:
[1, 100, 12, 25, 5]

Why it breaks:
By default, .sort() converts all elements to strings and compares their UTF-16 code units. Because "100" starts with "1", it comes before "2". Because "25" starts with "2", it comes before "5".

The Fix:
Always supply a comparator function for numeric sorting:

scores.sort((a, b) => a - b);
Enter fullscreen mode Exit fullscreen mode

6. The max() on an Empty Sequence

The Innocent Code:

def get_highest_bid(bids):
    return max(bids)
Enter fullscreen mode Exit fullscreen mode

What happens in testing:
You test with [100, 250, 40] -> Returns 250. Tests pass!

What happens in production:
A new auction opens with 0 bids. A user views the page.

Boom: ValueError: max() arg is an empty sequence.

The Fix:
Provide the default keyword argument:

def get_highest_bid(bids):
    return max(bids, default=0)
Enter fullscreen mode Exit fullscreen mode

7. The Float Precision Trap

The Innocent Code:

function applyDiscount(balance) {
    if (balance === 0.3) {
        return "Special promo applied!";
    }
    return "Regular rate";
}

const balance = 0.1 + 0.2;
console.log(applyDiscount(balance));
Enter fullscreen mode Exit fullscreen mode

What you expect:
"Special promo applied!"

What actually happens:
"Regular rate"

Why it breaks:
In IEEE 754 floating-point arithmetic (used by JS, Python, and most languages), 0.1 + 0.2 equals 0.30000000000000004. Exact equality comparisons on calculated floats will silently fail.

The Fix:
Compare within an epsilon:

if (Math.abs(balance - 0.3) < Number.EPSILON) {
    return "Special promo applied!";
}
Enter fullscreen mode Exit fullscreen mode

The Real Skill: Reading Broken Code

Every coding platform and university course teaches you how to write code starting from line 1 of an empty file.

Nobody teaches you how to read 300 lines of legacy code at midnight, spot the silent mutation, and fix it without breaking five other things.

If you want to train your eye to spot these bugs in real time, check out TryBugHunt:

  • 150+ interactive, real-world debugging challenges.
  • You get the symptom and broken code—never the answer.
  • Runs 100% client-side in your browser (powered by WebAssembly and Pyodide).
  • Includes escalating hints and mental model explanations.

It's completely free and requires zero login to start solving: trybughunt.com


What’s the most innocent-looking bug that has ever taken down your production system? Share your war stories in the comments!

Top comments (0)