A debugging interview gives you code that is broken or slow and asks you to find the problem, fix it, and explain what you did. You are judged less on whether you spot the bug in ten seconds and more on how you get there: do you reproduce it, narrow it down, test a guess, and check the fix. This guide covers what interviewers look for, the bugs that come up again and again, and a method you can follow under pressure.
What a debugging interview looks like
Formats differ by company, but three are common:
- Broken function with tests. You get a short program and a failing test, or a description such as “this returns the wrong answer for some inputs”. You find and fix the cause.
- Existing codebase. You get a small multi-file project in an editor, sometimes with a test suite that has a few failures. You read, run, and fix.
- Verbal walkthrough. The interviewer shows a snippet or an error message and asks how you would investigate. No typing needed, but your reasoning is the whole test.
Ask the recruiter which format to expect and whether you can use your own editor, a debugger, and the internet for documentation. Practice the format you will get. A related format is the code review interview, where you read code and comment on it instead of running it.
What interviewers are looking for
- A method, not luck. Reproduce, narrow down, hypothesize, test, fix, verify. Guessing and changing things at random is the most common way to fail.
- Reading speed. You can follow unfamiliar code and say what each function is supposed to do before you decide it is wrong.
- Root cause over symptom. Wrapping a crash in
try/excepthides the bug. Fix the thing that produced the bad value. - Clear communication. You say what you believe, what you will check, and what the result means, so the interviewer can follow and help.
- Verification. You run the failing case again, then try a few neighbouring cases to make sure the fix did not break something else.
A method you can follow under pressure
- Restate the expected behavior. “For an input of X, this should return Y.” If the spec is unclear, ask. Many bugs are really misunderstandings of what the code should do.
- Reproduce it. Run the code with the failing input. A bug you cannot trigger on demand is a bug you cannot confirm you fixed.
- Read the error first. A stack trace tells you the exception type and the line. Read from the bottom up: the last frame is where it failed, and the frames above it show how execution got there. Then ask where the bad value came from, which is often a different place.
- Shrink the input. Try the smallest input that still fails: an empty list, one element, two elements. Small inputs are easy to trace by hand.
- Form one hypothesis and test it. “I think the loop skips the last element.” Add a print statement or set a breakpoint to check that single claim. Do not change code to test a hypothesis you have not stated.
- Fix the cause with the smallest change. Avoid rewriting the function; in an interview that wastes time and hides what you found.
- Verify. Re-run the failing case, then the edge cases below, then the original passing cases.
Say all of this out loud. If you get stuck, say what you have ruled out. That lets the interviewer give a hint without giving away the answer, and it shows how you work.
Bugs that come up again and again
These examples are all runnable. Output shown is from running the code.
Off-by-one errors in loops
Task: count pairs of positions (i, j) with i < j. For a list of 3 items the answer is 3.
def count_pairs(nums):
c = 0
for i in range(len(nums)):
for j in range(i, len(nums)): # bug: j starts at i
c += 1
return c
print(count_pairs([1, 2, 3])) # 6, expected 3
The inner loop starts at i, so it also counts each position paired with itself. The fix is range(i + 1, len(nums)). Whenever a count is off by a small, regular amount, check loop bounds first. If you want practice spotting this kind of slip, the Buggy Code lesson on even numbers is a short exercise.
Binary search that loops forever or misses
Binary search is a favorite because three small details (lo <= hi, mid + 1, mid - 1) each break it in a different way. A version that does lo = mid instead of lo = mid + 1 can loop forever on some inputs. A correct version:
def binary_search(nums, target):
lo, hi = 0, len(nums) - 1
while lo <= hi:
mid = (lo + hi) // 2
if nums[mid] == target:
return mid
elif nums[mid] < target:
lo = mid + 1
else:
hi = mid - 1
return -1
print(binary_search([1, 3, 5, 7, 9], 7)) # 3
print(binary_search([1, 3, 5, 7, 9], 4)) # -1
print(binary_search([], 1)) # -1
Test it on an empty list, a single element, the first element, and the last element. Those four cases catch most binary search mistakes.
The mutable default argument (Python)
def add_item(item, items=[]):
items.append(item)
return items
print(add_item("a")) # ['a']
print(add_item("b")) # ['a', 'b'] (expected ['b'])
Python creates the default list once, when the function is defined, and every call without an argument shares it. The standard fix is to use None as the default and create the list inside:
def add_item(item, items=None):
if items is None:
items = []
items.append(item)
return items
print(add_item("a")) # ['a']
print(add_item("b")) # ['b']
Slow code that is technically correct
“This works, but times out on large inputs” is a debugging prompt too. A typical example is a primality check that tries every divisor up to n:
def is_prime(n):
for i in range(2, n):
if n % i == 0:
return False
return True
This is O(n) and it also has a correctness bug: it returns True for 0, 1 and negative numbers. Any divisor pair has one factor at most the square root of n, so you only need to check up to there:
def is_prime(n):
if n < 2:
return False
i = 2
while i * i <= n:
if n % i == 0:
return False
i += 1
return True
print([n for n in range(-1, 20) if is_prime(n)]) # [2, 3, 5, 7, 11, 13, 17, 19]
print(is_prime(1_000_000_007)) # True
Now it is O(sqrt(n)). In an interview, say the complexity before and after, and mention the edge cases you fixed along the way. Start by finding where the time goes, not by guessing: look for a nested loop, repeated work, or a data structure with slow lookups, such as searching a list when a set would do.
Race conditions
Two threads updating shared state without coordination lose updates, because balance += amount is a read, an add, and a write, not one step.
public class Main {
static int unsafe = 0;
static int safe = 0;
static synchronized void deposit() { safe++; }
public static void main(String[] args) throws Exception {
Thread[] threads = new Thread[4];
for (int i = 0; i < 4; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 100000; j++) {
unsafe++; // not atomic
deposit(); // synchronized
}
});
threads[i].start();
}
for (Thread t : threads) t.join();
System.out.println("unsafe lost updates: " + (unsafe < 400000));
System.out.println("safe = " + safe);
}
}
Output: unsafe lost updates: true and safe = 400000. The exact value of the unsafe counter changes from run to run, which is the sign of a race: the bug is real but not repeatable on demand. For a counter, AtomicInteger is simpler than synchronized. For a bank account with several fields, guard all of them with the same lock. If you hold two locks at once, always take them in the same order, or you invite a deadlock.
Memory leaks
In C++ the classic example is a class that calls new[] in its constructor and has an empty destructor. The fix is to release the memory in the destructor, or better, not to manage it by hand:
#include <cstdio>
#include <vector>
struct Fixed {
std::vector<int> data;
Fixed() : data(1000) {} // freed automatically
};
int main() {
Fixed f;
std::printf("%zu\n", f.data.size()); // 1000
}
If the interviewer insists on raw pointers, add delete[] data; to the destructor and mention the rule of three: a class that owns memory also needs a correct copy constructor and copy assignment, or you trade a leak for a double free. In garbage-collected languages the equivalent leak is a long-lived collection (a cache or a listener list) that keeps references to objects you no longer need.
SQL injection
Building a query by pasting user input into a string lets the input change the query:
import sqlite3
c = sqlite3.connect(":memory:")
c.execute("create table users(username text, secret text)")
c.execute("insert into users values ('ann','a-secret'), ('bob','b-secret')")
name = "x' OR '1'='1"
print(c.execute(f"SELECT * FROM users WHERE username = '{name}'").fetchall())
# [('ann', 'a-secret'), ('bob', 'b-secret')] every row leaks
print(c.execute("SELECT * FROM users WHERE username = ?", (name,)).fetchall())
# [] the input is treated as data
The fix is a parameterized query (a placeholder plus a separate value), which works the same way in PHP’s PDO and mysqli prepared statements, Java’s PreparedStatement, and every other mainstream database library. Escaping quotes by hand is not an acceptable answer.
A note on “the fibonacci bug”
A lot of practice lists include a recursive fibonacci that “returns the wrong value for 0”. With the usual base case if n <= 1: return n it is correct: fib(0) is 0, fib(1) is 1, fib(10) is 55. If an interviewer claims there is a bug, check by running it. The real problem with that function is speed: it recomputes the same values exponentially many times, and memoization (or a loop) fixes that. Sometimes the correct answer in a debugging interview is “this part is fine, here is how I verified it”.
Edge cases to check after every fix
- Empty input, a single element, two elements.
- Zero and negative numbers; the smallest and largest allowed values (and integer overflow in Java, C++, and Go).
- Duplicates, and input that is already sorted or sorted in reverse.
null/Nonewhere it is allowed.- Division by zero.
sum(nums) / len(nums)on an empty list raisesZeroDivisionError, and the right behavior (raise, return 0, return None) is a question for the interviewer. - Shared state: a second call to the same function, or two threads calling it.
The same habit of tracing a small example by hand helps in normal coding rounds. See how to dry run code with sample test cases.
Tools worth knowing before the interview
- Print debugging. Fast and works everywhere. Print the values that matter with labels (
print("lo", lo, "hi", hi)), and remove them before you finish. - A real debugger. Learn to set a breakpoint, step over and step into, and inspect variables in your IDE, or use
pdbin Python (breakpoint()drops you into it) andgdbfor C and C++. Stepping through a loop beats guessing. - Reading stack traces. Practice with real error messages in the language you will interview in. Common ones (syntax errors, undefined names) are covered in the Name Errors lesson.
- Tests. If the project has a test suite, run it first and read the failing test names. They often point straight at the area.
- Sanitizers and profilers. For C and C++, AddressSanitizer and LeakSanitizer (
-fsanitize=address) find memory errors; for slow code, a profiler shows where time goes. Mention them even if you can’t run them in the interview.
Mistakes that cost candidates the round
- Changing several things at once. If the output improves, you won’t know which change helped, and you may have added a new bug.
- Fixing the first thing that looks odd. Style problems and bad names are not the bug. Confirm that the line you want to change can produce the failure you saw.
- Silence. Ten minutes of quiet reading gives the interviewer nothing to evaluate or help with.
- Suppressing the error. Catching the exception, adding a null check, or clamping a value until the test passes is not a fix unless you can explain why the bad value appeared.
- Not re-running. Say “I’ll run the failing case again” and do it, then check the neighbors.
How to practice
- Take solutions you wrote earlier, introduce one bug of each type above, and put them away for a week. Then debug them cold.
- Trade broken snippets with a friend, and time yourself while narrating.
- Fix a failing test in a small open-source project. Reading someone else’s code under a real failure is close to the real interview.
- Do exercises that give you broken code instead of a blank editor. AlgoCademy has “Buggy Code” lessons in its Python and JavaScript courses, and you can start from the app.
- Before each practice session, write the six-step method on paper until you can recite it without looking.
Debugging is a skill that improves with repetition, and the method stays the same across languages: reproduce, shrink, hypothesize, test, fix, verify.
