Most bugs are not clever. They are the same few slips, made in every language, that look right at a glance and quietly give the wrong answer. Learning to recognise them is one of the fastest ways to get better at reviewing and debugging code.
Each bug below is one you can practise spotting in Bug Hunt, which shows short JavaScript, Python and SQL snippets with one broken line each.
Off by one
A loop that runs once too often or once too few is the classic bug. In JavaScript, for (let i = 0; i <= items.length; i++) reads one past the end, where items[i] is undefined. In Python, range(1, 10) stops at 9, and the slice items[-3:-1] leaves out the last item.
Habit: check the first and last pass of every loop, and remember that ranges and slices stop before their end value.
Comparisons that never succeed
Some values are not equal to anything, including themselves. In JavaScript, x === NaN is always false; use Number.isNaN(x). In SQL, phone = NULL is never true; use phone IS NULL. In Python, is checks for the same object, so role is "admin" can be false even when the text is equal; use ==.
Floating-point numbers belong here too: 0.1 + 0.2 == 0.3 is false in nearly every language. Compare floats with a tolerance, such as Python’s math.isclose.
Zero is falsy
In JavaScript, level || 50 falls back to 50 when level is 0, because 0 is falsy. The same trap catches empty strings. Use level ?? 50, which falls back only for null and undefined.
A relative of this bug: if (list.indexOf(x)) is false when x is the first item (index 0) and true when it is missing (-1). Use includes.
Values that are shared when they look copied
- Python creates a default argument once.
def add(tag, tags=[])reuses the same list on every call; default to None and make the list inside. [[0] * 3] * 3in Python repeats one inner list three times, so setting one cell sets a whole column.- A JavaScript spread,
{ ...defaults }, copies only the top level. Nested arrays and objects are still shared.
Habit: whenever data is copied, ask whether anything inside it is a list or an object.
Functions that do not return what you think
Python’s list.sort() sorts in place and returns None; sorted() returns a new list. Strings never change, so text.upper() on its own line does nothing useful. In JavaScript, sort() without a compare function sorts numbers as text, putting 100 before 25.
["10", "10", "10"].map(parseInt) gives [10, NaN, 2], because map passes the index as parseInt’s second argument, the radix.
Async code that is not awaited
items.forEach(async (item) => await save(item)) does not wait for anything: forEach ignores the promises, so the code after it runs at once. Use for...of with await inside, or await Promise.all(items.map(save)).
Closures that see the final value
A function made inside a loop sees the loop variable as it is when the function runs, not when it was made. With var in JavaScript, three timeouts all log 3; let gives each pass its own variable. Python lambdas behave the same way, so lambda x: x * n in a loop uses the last n; bind it with a default, lambda x, n=n: x * n.
SQL that drops rows
- A condition on the right-hand table of a LEFT JOIN, written in WHERE, removes the rows with no match. Put it in the ON clause.
NOT IN (subquery)returns nothing if the subquery yields a NULL.COUNT(column)skips NULLs;COUNT(*)counts every row.BETWEENwith a date on a timestamp column stops at midnight at the start of the last day.- AND binds tighter than OR, so mixed conditions need parentheses.
How to get faster at spotting bugs
Read the goal first, then look for the input that would break the code: an empty list, zero, NULL, the last item. Check every comparison operator and every boundary. And practise on short snippets where you know a bug is there; Bug Hunt gives you eight a round, with the cause and the fix after each.