Debugging Interviews
Debugging interviews flip the script on the usual coding interview: instead of writing code, you're mostly reading existing code that you're given. The goal is to understand code you didn't write, form and test hypotheses, and implement a fix.
AI can generate code that looks plausible. Debugging shows whether you can test that plausibility against a real system and keep working when the first explanation is wrong.
The three types of debugging rounds
1. Code debugging
You're given a codebase with one or more known issues and a bug report: a failing test, wrong output, or a crash. Reproduce the issue, find its cause, and fix it. This is the most common format, and it increasingly includes a coding agent.
2. Code review, or "spot the bug"
You're asked to review a file, a pull request, or raw logs and identify potential issues. The review itself may be the deliverable: which comments would you leave, and on which lines? This is especially common for security engineers who are expected to find vulnerabilities.
3. Root cause analysis
Instead of receiving code, you reason through a scenario with the interviewer: "Requests to the API are timing out. What would you do?" There is nothing to run, so walk through what you would inspect, in what order, and what each result would rule out. This format is common for reliability engineers (SREs), backend engineers, and full-stack engineers.
Write your hypotheses down as you go. This keeps you from retesting something you've already ruled out and gives the interviewer a visible record of your reasoning.
The debugging process: reproduce, hypothesize, prove
Reproduce first. Find the smallest reliable reproduction, ideally one failing test or one command with one input. A smaller case rules out every path it no longer touches.
Read the error before you theorize. Follow the stack trace to the exact line instead of pattern-matching on the message. The frame you want is usually the last one inside your own code.
Name one hypothesis at a time, out loud. "The delay is zero on every attempt, so my hypothesis is that the backoff multiplier is applied before the first retry rather than compounding." You can check that claim in one step. "Something's wrong with the retry logic" gives you nothing specific to test.
Prove it before you fix it. Instrument, observe, then confirm the hypothesis or discard it and take the next one.
Example bugs to watch out for
When you need a starting point, check the common sources of bugs:
- Boundary conditions. Empty input, a single element, the first and last iteration, an off-by-one in an index or a range.
- State changes. Something changes between two reads: a stale cache, an object shared across calls, a mutable default argument, a variable captured by a closure.
- Race conditions. Two async operations occur simultaneously; an async operation or promise is not awaited properly; or the order of operations is incorrect.
- Input and data. The data doesn't match the schema you assumed: nulls, duplicates, encodings, numbers arriving as strings.
- Environment. Config never loads, a dependency version drifts, the process runs a different build than the one you're reading.
Debugging with print statements
Every language has its own debugging tools. JavaScript has the debugger keyword for breakpoints, and Python has pdb. Learn the tools for your main language, but do not overlook the simplest option: a well-placed print statement.
A useful print statement answers two questions: Where am I? and What do I know here? Add a location and the values relevant to your hypothesis. For example, compare these approaches to debugging an API retry loop:
# BAD: no way to tell which line printed which, and no values.
print("test")
print(x)
print("here")
# GOOD: tagged location, plus the arguments that matter.
print(f"[retry_loop] attempt={attempt} delay={delay}ms max={max_delay}")The useful version prints [retry_loop] attempt=3 delay=0ms max=8000. That gives you enough information to confirm or reject a specific hypothesis.
Print on both sides of the suspicious call. Seeing what goes in and what comes out beats inferring one from the other, and it tells you immediately whether the bug is upstream or downstream of the line you're staring at:
print(f"[parse] before: raw={raw!r}")
record = parse(raw)
print(f"[parse] after: record={record!r}")Print data and confirm its type too. "0" and 0 are indistinguishable in output and behave nothing alike, and the same goes for None versus "None" and [] versus "":
print(f"[validate] user_id={user_id!r} type={type(user_id).__name__}")What to do when you're stuck
At some point, the trail may go cold. Say what failed to match your mental model: "I expected the config to be loaded by this point, and it isn't, so my model of the startup order is wrong somewhere." Re-read any code you skimmed, return to the last thing you proved, and work forward. In an interviewer-driven format, ask whether more logs, metrics, or data are available.
Do not go silent or make speculative edits until something happens to work. A late fix backed by a clear investigation is easier to defend than a lucky edit you cannot explain.
Using AI to help debug
When AI is allowed, use it to suggest possible causes, challenge a hypothesis, or propose a fix after you've narrowed down the issue. A coding agent can also add print statements and instrumentation across the codebase while you decide what evidence to collect.
Here are a few prompts you can adapt:
Interpreting a stack trace. Ask for the explanation and not an automatic fix, so you stay the one deciding what to do:
PseudocodeHere's a stack trace and the file it points into. Walk me through what this trace says happened, and tell me which method or class is responsible. Suggest potential solutions but don't implement them yet.
Orienting in an unfamiliar codebase. Ask for a map before you form a hypothesis:
PseudocodeGive me a tour of this codebase in under 15 lines: the entry point, the main flow a request takes through it, and which files own which responsibility. List anything that looks like a seam where behavior gets added or intercepted.
Getting candidate causes from a symptom. Give the model the report and the constraint, and ask it to rank rather than assert. Treat its suggestions as hypotheses to verify, just like your own.
PseudocodeSymptom: the /search endpoint returns correct results for the first request after startup and empty results for every request after that. Nothing in the logs. Based on this code, list the 5 most likely causes ranked by probability, and for each one give me the single cheapest check that would confirm or rule it out.
Verifying before you accept. When the agent proposes a fix, make it argue against itself:
PseudocodeBefore I apply this: which line specifically causes the bug I described, and why does the current code produce the symptom I'm seeing? If you're inferring the cause rather than tracing it, say so. Also confirm every function you referenced actually exists in the files I gave you.
Use AI to speed up the loop while you remain responsible for it. State the division of labor out loud: the agent is instrumenting and summarizing, while you decide what the evidence means.
Practice problems in this module
Find and fix the bugs behind four incorrect loyalty statements, with a written spec as the source of truth and no test suite.
Diagnose a production outage from logs, metrics, and service configuration, then decide what to do while it's still happening.