📒 problems 📖 glossary
Mock interview
46:30
ready
of 46:30
Lesson 64 · Modern signals

"Here's broken code — find the bug"

📖 Walk me through it — plain English

A "debugging round" is an interview where they hand you code that is already broken and ask you to find and fix the bug while they watch. The twist: they barely care whether you find the bug. What they are really grading is how you hunt for it. "Debugging" just means the detective work of figuring out why a program does the wrong thing. They want to see calm, methodical reasoning — not panic.

Think of it like a doctor with a sick patient. A bad doctor guesses — "let's try this pill, then that one" — and hopes something works. A good doctor takes the patient's history, runs one cheap test, reads the result, forms a theory, then confirms it before prescribing anything. Same illness, totally different process. In this round, randomly tweaking lines until the error disappears is the "guessing doctor," and it reads as flailing even if you stumble onto the fix.

The lesson gives you a repeatable 5-step loop so you never flail. Here is the loop in plain terms:

1 · Reproduce. Make the bug happen on demand with the smallest possible input. If you cannot trigger it reliably, you are debugging blind. ("Minimal example" = the tiniest case that still breaks.)
2 · Read carefully. Read every line of the broken function, slowly. Skimming is exactly how experienced engineers stare past the bug for twenty minutes.
3 · Say your hypothesis out loud. A "hypothesis" is just your best guess at the cause, e.g. "I bet the loop stops one step too early." Naming a suspect out loud commits you to checking it instead of wandering.
4 · Test it cheaply. Confirm or kill the guess with the least effort — a print/log statement, a debugger, or best of all a tiny test that fails only if your guess is correct.
5 · Bisect when stuck. "Bisect" = cut the search space in half. Disable half the code; if it still breaks, the bug is in the other half. (git bisect does this across past commits when something "worked yesterday.")

The middle of the lesson is a cheat sheet of the usual suspects — the bugs that show up again and again: an off-by-one (using < where you meant <=, so you process one too many or too few items), a stale closure (a saved function remembers an old value of a variable), type coercion (the language silently converts types, so "10" < "9" is true because it compares text not numbers), an async ordering race (two tasks finish in the wrong order), mutation aliasing (you thought you copied an object but two names point at the same one), and a silent error swallow (a catch block hides the real error). Memorizing this list means step 3 gets faster — you have a line-up of suspects to check.

Why this works: the loop turns a scary open-ended "find the needle" task into small, narrated, falsifiable steps. Each step either confirms or eliminates a guess, so you steadily close in instead of spinning. And because you are thinking out loud the whole time, the interviewer sees a reliable engineer — the exact signal they came to measure. The closing verbal move seals it: before touching anything, say "let me confirm what this should do versus what it actually does." Pinning down that gap points the whole investigation in the right direction.

Live-debugging rounds test how you reason under uncertainty. The candidates who flail try random fixes. The candidates who pass narrate hypotheses, test them, and converge.

First, the words — defined inline

Before the steps, lock down the vocabulary so nothing below is a mystery. A few of these terms get thrown around loosely; here is exactly what each one means in this context.

  • The debugging round — a timed interview (often 30–45 min) where you are given a short program that produces wrong output or crashes, and asked to find and fix the defect while talking through your thinking. Unlike a normal coding round, you do not design from scratch; you investigate something that already exists.
  • Bug / defect — any place where the code's actual behavior differs from its intended behavior. The intended behavior is the "spec" (specification), even if the spec is just one sentence the interviewer said.
  • Reproduce — to make the bug appear on command. A bug you can only see "sometimes" is much harder to fix than one you can trigger with a single known input. Reproducing first means every later experiment has a reliable before/after to compare against.
  • Isolate — to shrink the problem until only the broken part is left. You strip away inputs, code paths, and data that are not involved, so the remaining suspect surface is small. A "minimal reproducing example" (often shortened to minimal repro) is the smallest input + smallest slice of code that still shows the bug.
  • Bisect — to locate a defect by repeatedly halving the search space. Each test tells you which half contains the bug, so the number of suspects drops by half every step. The same idea powers git bisect, which binary-searches your commit history to find the exact commit that introduced a regression.
  • Hypothesis-driven debugging — a method where, instead of changing code at random, you state a specific, testable guess ("the loop runs one time too few"), then run the cheapest experiment that would prove that guess right or wrong. Each experiment either confirms the cause or eliminates it; you never change code without first predicting what the change should do.
  • Regression — a bug in something that used to work. The word literally means "going backward": a change made the software regress to a worse state. When you hear "it worked yesterday," you are hearing about a regression, and bisecting across commits is the fastest way to catch the change that caused it.

The whole craft in one sentence: reproduce the failure, isolate it to a small surface, then form one hypothesis at a time and test it cheaply — changing exactly one thing per experiment so you always know what caused the change in behavior.

The 5-step loop
  1. Reproduce. Run the failing case. If you can't reproduce, you can't debug. Get a minimal example.
  2. Read carefully. Don't skim — read every line of the function the bug lives in. Skimming is how seniors miss it for 20 minutes.
  3. Form a hypothesis out loud. "I think the bug is the off-by-one in the partition boundary." Naming the suspect commits you to a test.
  4. Test the hypothesis cheaply. Print, log, debugger, or — best — write a test that would fail iff your hypothesis is right.
  5. Bisect when stuck. Comment out half. Still fails → bug's in the other half. git bisect for "worked yesterday."

The loop is a cycle, not a checklist you do once. After step 4 you either fixed the bug (done) or you learned which guess was wrong (go back to step 2 with a smaller suspect surface). Each pass through the loop either ends the hunt or shrinks it. That guarantee — every step makes progress — is what keeps you from spinning.

A worked walkthrough: a buggy "average" function

Theory only sticks once you watch the loop run on real code. Here is a small function meant to return the average of a list of numbers. It is short enough to fit on a slide, which is exactly the size of bug you will see in this round.

def average(nums):
    total = 0
    for i in range(1, len(nums)):
        total += nums[i]
    return total / len(nums)

print(average([2, 4, 6]))   # expected 4.0 ... but prints 3.3333...

Now run the loop out loud, the way you would in the room.

  • Reproduce. "The spec is: average of [2, 4, 6] should be (2+4+6)/3 = 4.0. It prints 3.333 instead. I can trigger it on demand with that one input, so I have a reliable repro." Notice the very first move is stating should vs. does — the gap is the whole investigation.
  • Read carefully. "Three numbers, sum should be 12, divided by 3. The result 3.333 is 10/3. So the divisor 3 looks right, but the sum is 10, not 12. The number that got dropped is 2 — the first element."
  • Form a hypothesis out loud. "I bet this is an off-by-one in the loop bound. range(1, len(nums)) starts at index 1, so it skips nums[0], which is exactly the 2 that went missing. My hypothesis: the loop should start at index 0." This is hypothesis-driven debugging — one specific, falsifiable guess, not a vague "the loop is wrong."
  • Test it cheaply. "Cheapest confirmation: I'll predict the symptom my guess implies and check it. If index 0 is skipped, then the computed sum should equal 12 - nums[0] = 10, and indeed 10/3 = 3.333 matches the output exactly. That confirms the hypothesis without even editing code." (Even cheaper as a one-liner: add print(total) just before the return — it prints 10, the missing-first-element fingerprint.)
  • Fix and re-verify. "Change range(1, len(nums)) to range(0, len(nums)) — or simply range(len(nums)). Re-run the repro: now 2+4+6 = 12, 12/3 = 4.0. Matches the spec." Then check the boundaries the cheat sheet warns about: empty list (would divide by zero — worth flagging), single element, two elements. Tracing those edge cases out loud is itself a strong signal.

The key discipline: one change, one prediction. You predicted the sum would be 10, you saw 10, you changed exactly one thing (the start index), and you re-ran the same repro to confirm. At no point did you alter two lines at once and hope. That is the difference between converging and flailing.

Off-by-one

< vs <=, arr.length vs arr.length - 1. Trace boundary inputs: empty, single, two. (This is the bug in the walkthrough above — range(1, ...) skipped index 0.)

Stale closure

Callback captured an old variable. React useEffect / setTimeout in a loop classics. Log the captured value.

Type coercion

"0" == false is true in JS. "10" < "9" is true (string compare). Use === and explicit conversions.

Async ordering

A finishes before B but you depend on B. Race. Use await / Promise.all / proper sequencing.

Mutation aliasing

Two refs to the same object. "I only changed copy" — you didn't copy. Spread / structuredClone fixes shallow cases.

Silent error swallow

try { ... } catch {} with no log. Always log or rethrow.

Why keep this list in your head? It turns the slowest step — forming a hypothesis — into a fast lookup. The symptom usually points at a category: wrong output at the edges of a range smells of off-by-one; a value that is "stuck" at an old reading smells of a stale closure; "10" < "9" returning true smells of type coercion; output that changes run-to-run smells of async ordering; "I edited the copy but the original changed too" is mutation aliasing; and an error that vanishes without a trace is a swallowed exception. Match symptom to suspect, then test that one suspect.

When the broken code came from AI

On the job — and increasingly in take-homes where AI use is allowed — the snippet you are debugging may be code you or the AI wrote five minutes ago. The loop above still applies: reproduce, hypothesize, test one thing. What changes is your suspect list. AI output fails in shapes humans rarely produce on purpose:

Invented APIs

Calls a method or import that does not exist. Cross-check against the actual repo and language docs before you theorize about logic bugs.

Happy-path-only logic

Works on the example in the prompt; empty, null, or boundary input crashes. Always trace one non-happy input before accepting a fix.

Partial multi-file edits

Changed the callee but not the caller, or updated a type in one file only. Grep the changed symbol across the repo.

Symptom patches

AI "fixes" often silence the error without fixing the cause — extra null checks, broad try/catch. Ask "but why was it null?" before merging.

The trap: re-prompting "fix this bug" before you have a reproducer. That produces a new confident wrong answer and destroys your ability to bisect. Reproduce first, name the failure, then optionally use AI to brainstorm hypotheses — same discipline as the AI collaboration and AI-assisted coding round lessons. On the job, the verification habit from those lessons is debugging AI code.

More industry shorthand (debugging round)
  • Root cause vs symptom — fix the cause, not just where it hurts. Say the root cause in one sentence — that's often what wins the round.
  • Band-aid / symptom patch — change that hides the error without explaining why.
  • git blame — who last edited this line (not about blame — it's archaeology).
  • Divide and conquer — same as bisect: halve the search space each step.
  • Works on my machine — env mismatch; check versions, env vars, lockfile.

What interviewers actually look for

Remember the framing: finding the bug is necessary but not sufficient. A candidate who blurts the right fix in ten seconds with no reasoning often scores worse than one who narrates a clean hunt and finds it at the buzzer, because the interviewer cannot tell whether the fast candidate was lucky. They are scoring the process. Concretely, the positive signals are:

  • You reproduce before you theorize. Confirming the failure first shows you debug from evidence, not vibes.
  • You state the spec. "Here's what it should do vs. what it does" proves you understand the goal — you cannot fix toward a target you never named.
  • You narrate falsifiable hypotheses. Each guess is specific enough to be proven wrong by a cheap experiment, and you say what result would confirm or kill it before running it.
  • You change one thing at a time. One edit, one prediction, one re-run. This is the single clearest marker of a disciplined debugger.
  • You re-verify and check edges. After the fix, you re-run the repro and trace boundary inputs (empty, single, two, very large) instead of declaring victory on one case.
  • You stay calm and think out loud. Even when stuck, you say what you'd try next and why. Silence reads as panic; a narrated dead-end reads as competence.

Pitfalls that sink candidates

  • Changing many things at once. The cardinal sin. If you edit three lines and the bug disappears, you have no idea which edit mattered — and you may have introduced two new bugs that cancel out. One variable per experiment, always.
  • Skipping reproduction. Jumping straight to fixes means you are fixing a bug you have not actually seen. You waste time "fixing" code that was never the problem.
  • Random tweaking ("shotgun debugging"). Trying changes with no hypothesis, hoping one sticks. Even if it works, it reads as flailing and teaches you nothing about the cause.
  • Skimming the code. Reading fast past the bug is how engineers lose twenty minutes. Read every line of the suspect function once, slowly.
  • Rewriting from scratch. Tempting under stress, but it throws away the one thing you have (a reproducible failure) and the interviewer learns nothing about your debugging.
  • Declaring victory on one input. The fix that passes [2,4,6] might still crash on the empty list. Re-verify the original repro and the boundaries before you stop.
  • Going silent. If you stop narrating, the interviewer cannot follow your reasoning — and the reasoning is what they are grading.
Verbal move: "Before I fix anything, I want to confirm I understand what it should do vs. what it does." Stating the gap forces the right kind of investigation.

Go deeper (optional): the canonical reference on this mindset is the chapter "Understand the System" in Debugging by David J. Agans, whose nine rules (start with "Quit Thinking and Look," "Make It Fail," and "Change One Thing at a Time") are the same loop described above. You do not need it to pass — everything required is on this page — but it is a good lifelong reference.

Go deeper (optional, other guides): Modern Web Dev Guide — Debugging methodology covers production incidents, git bisect, profilers, and debugging AI-drafted code on the job. AI Guide — LLM debugging playbook covers when the bug is in the model pipeline, not your Python loop.

→ Going deeper: Single-file debugging escalates to unfamiliar repos. See Real-codebase debugging.
→ Going deeper: AI-drafted code uses the same loop with different suspects — see AI collaboration and AI-assisted coding round.
→ Going deeper: Debugging rounds rehearse the observability mindset. See Observability & SRE.