API Design

Debugging & Real-World · Lesson 03

Acing the Stripe-style debugging interview

Stripe's engineering interviews include rounds that put you inside an unfamiliar codebase with a failing test and a ticking clock. The goal is not to show you know the codebase — you've never seen it. The goal is to show you can find your way through any codebase under pressure. This is a learnable skill with a concrete playbook.

⏱ 22 min Difficulty: advanced Prereq: dbg-01, dbg-02

By the end you'll be able to

What the rounds actually are

Stripe's technical interview loop is known for going beyond algorithm exercises. Two rounds in particular focus directly on the kind of work engineers do every day in production systems.

The Bug Squash round

You are given access to a real, unfamiliar codebase — often a pinned version of an open-source library, or a simplified internal service — that has a known bug introduced into it. That bug corresponds to a failing unit or integration test, which you can run. The task is to find the bug, understand why it's wrong, fix it, and confirm the test passes. Time is approximately 45–60 minutes.

You work in your own IDE, on your own machine, in your preferred language. There is no trick syntax or exotic API to memorize. The codebase is real code, the bug is a real kind of bug (an off-by-one, a wrong default, a missing guard), and your tools are your real tools.

The round is called "Bug Squash" because the task is literal — squash a specific bug — but the name undersells what's being measured. See the next section.

The Integration round

A related round sometimes called the Integration round gives you a realistic API (often Stripe-shaped — an API key, real-looking endpoints, a webhook flow) plus a starter project with some code already written. The task is to extend or fix the integration: handle a webhook event correctly, add a retry with idempotency, or wire up a new endpoint. This tests how you work with an unfamiliar API under time pressure — reading documentation quickly, identifying the relevant endpoint, and writing working code against it.

The two rounds together cover the two most common professional scenarios: debugging code you didn't write, and integrating an API you haven't used before.

Other rounds in the loop

Stripe also runs an API design round (design a resource model and HTTP contract for a real-world scenario), a system design round (architecture for a component at scale), and a writing exercise (a clear technical document — an RFC, a design doc, or a short explanation). This lesson focuses on the Bug Squash and Integration rounds; the API design and system design rounds are covered elsewhere in this course.

What the interviewers are actually scoring

This is the single most important thing to understand about the Bug Squash round: the interviewers are not primarily scoring whether you found the bug. They are scoring your process. A candidate who finds the bug by luck after 10 minutes of random changes scores lower than a candidate who methodically narrows to the bug in 40 minutes and explains each step clearly.

The specific signals they look for, in roughly this priority order:

  1. Fast, confident orientation in an unknown codebase. Do you navigate by running the failing test first? Do you follow the call stack rather than reading every file top-to-bottom? Do you use search and jump-to-definition rather than manual scrolling?
  2. Hypothesis-driven narrowing. Do you form a specific, testable hypothesis before changing anything? "I think the off-by-one is in the pagination calculation on line 87" is a hypothesis. "Let me just change things and see what happens" is not.
  3. Reproducing first, then fixing. Can you run the failing test and confirm the failure before touching the code? Candidates who start changing code before confirming the reproduction frequently over-fix and break other tests.
  4. A targeted fix that addresses the observed failure. The fix should match the diagnosed cause exactly. Changing five things at once is a red flag — it suggests you're not confident in your diagnosis.
  5. Validating with the test suite. After the fix, run the full test suite, not just the originally failing test. The bug fix should not break anything else.
  6. Clear narration throughout. Talk through what you're seeing. "I'm running the test now — it's failing with an AssertionError on line 23 of the test. The expected value was 10 but got 11. That suggests an off-by-one somewhere in the calculation. Let me find where that value is computed." The interviewer should never wonder what you're thinking.
🎯 The counterintuitive scoring rule

A clear, narrated diagnosis that reaches the correct root cause — even if the fix is incomplete or you run out of time — can score higher than a correct fix achieved through silent trial-and-error. Interviewers are hiring for how you work every day, not for how quickly you get lucky. Say what you're thinking, especially when you're uncertain.

1. Reproducethe failure 2. Orientin the repo 3. Hypothesizethe cause 4. Inspect(debugger) 5. Fixsmallest change 6. Validate ✓ if wrong, re-hypothesize Narrate every step out loud — the interviewer scores your reasoning, not just the patch
The loop the round rewards: reproduce before fixing, narrow with a debugger, change the least possible, prove it with the tests — talking the whole way.

The ten-step playbook

Apply this sequence every time, regardless of the language or the codebase. The steps are ordered to minimize wasted work: you confirm what you know before investigating what you don't.

  1. Read the issue or failing test carefully. Before touching anything, read the full error message or the failing test assertion. What value was expected? What was actually returned? What's the name of the failing test — does it describe a specific behavior? This first minute often tells you which part of the codebase to look at.
  2. Reproduce: run the failing test, confirm the failure. Run the test suite and watch the failure happen. Do not proceed to investigation until you can reproduce it on demand. Note the exact assertion that fails, the exact line, and the exact error message. This is your ground truth — everything you do next should be in service of explaining this specific failure.
    npm test -- --grep "calculates shipping cost" 1 failing 1) ShippingCalculator calculates shipping cost AssertionError: expected 12 to equal 10 at Context.<anonymous> (test/shipping.test.js:18:5) # Good. The failure is confirmed. Expected 10, got 12. Off-by-2, or a wrong base rate.
  3. Orient: map the repo structure, find the relevant path. Spend two to three minutes understanding how the repo is laid out. Where is the test file? What function does it call? Use grep or your IDE's "go to definition" to jump from the test to the production code. Read only the code that is directly on the call stack of the failing test — do not read unrelated modules.
    grep -rn "calculates shipping cost\|ShippingCalculator" . --include="*.js" ./test/shipping.test.js:14: it('calculates shipping cost', ... ./src/shipping.js:1:class ShippingCalculator { # Now read src/shipping.js — specifically the method called by the test.
  4. Hypothesise: form one specific, testable claim. Based on what you read in the test and the production code, write down a hypothesis. It should name the specific line, variable, or logic path you suspect and predict what evidence would confirm or disprove it. "The base shipping rate should be 5, but line 14 uses 7 — changing it to 5 should make the test pass" is a hypothesis. Write it in a scratch note. If you're wrong, you'll update it, and having the wrong hypothesis written down is itself useful.
  5. Use a debugger — not print statements. Set a breakpoint at the entry point of the failing function. Step through. Watch the exact value at the exact line where you hypothesise the bug lives. A debugger gives you precision that console.log cannot: you can inspect any variable at any point in the call stack, not just the ones you thought to print. The ability to use your IDE's debugger fluently — breakpoints, conditional breakpoints, step-in vs step-over vs step-out, the call stack panel, watch expressions — is a force multiplier in this round.
    # In your IDE: set breakpoint on line 14 of src/shipping.js # Run test in debug mode # Inspect 'baseRate' in watch panel → breakpoint hit: shipping.js:14 baseRate = 7 ← expected 5 distanceFactor = 1.4 total = 7 * 1.4 = 9.8 → ceil → 10... wait, but test expected 10 and got 12 # Hypothesis was partially right but incomplete — keep stepping
  6. Bisect / narrow further if needed. If the debugger reveals your first hypothesis was incomplete, update it with the new information and continue stepping. Don't abandon the systematic approach. If the function calls other functions, step into them. A bug that spans two functions is harder to spot by reading but easy to catch when you're watching values change.
  7. Make the smallest possible fix. Once you've confirmed the root cause — the exact line and the exact wrong value — make the smallest change that corrects it. Change the wrong constant, fix the off-by-one boundary condition, add the missing guard clause. Do not refactor surrounding code. Do not "improve" things you weren't asked to change. Scope creep in a timed interview is a common way to introduce new failures.
    // Before (the bug)
    const baseRate = 7;   // was: wrong default
    
    // After (the fix)
    const baseRate = 5;   // matches the documented base shipping rate
  8. Re-run the full test suite. Not just the originally failing test — the full suite. Your fix should make the failing test pass and leave all other tests green. A fix that breaks other tests is worse than no fix; it means you misunderstood the scope of the change.
    npm test 24 passing (1.2s) # All tests pass. The fix is complete.
  9. Consider edge cases and regressions. After the tests pass, ask yourself: "Is there a related boundary condition that might also be wrong? Is there another caller of this function that might be affected?" If the interview time allows, mention these aloud even if you don't fix them — it signals a complete mental model.
  10. Narrate throughout and ask clarifying questions. Keep talking. Tell the interviewer what you're reading, what you're hypothesising, what the debugger is showing you. If something is ambiguous — "this test description isn't quite matching what I see in the code" — ask. Interviewers are not trying to trick you; they want to see how you handle ambiguity, and asking good clarifying questions is part of the signal.

Worked walkthrough: the OffsetPaginator bug

Here is an original worked example showing the full arc of the Bug Squash experience. The language is Python. The repo is a fictional pagination utility.

Setup

You've been given access to a small Python repo, paginator-lib, with the instruction: "There is a failing test. Find and fix the bug." You have 45 minutes. The repo has one module (paginator.py) and one test file (tests/test_paginator.py). You've never seen this codebase before.

Step 1–2: Read the test, reproduce

python -m pytest tests/test_paginator.py -v FAILED tests/test_paginator.py::test_last_page_is_complete AssertionError: assert 10 == 8 Where: test_paginator.py line 31 # Confirmed. Expected 8 items on the last page; got 10.

You read the failing test:

# tests/test_paginator.py:25–32
def test_last_page_is_complete():
    # 28 items, page size 10 → pages of 10, 10, 8
    p = OffsetPaginator(total_items=28, page_size=10)
    last = p.page(3)          # 0-indexed: page 3 is the third page
    assert last.item_count == 8  # should be 8, not 10

Step 3: Orient

grep -n "item_count\|def page\|offset" paginator.py 12: def page(self, page_number): 18: item_count = min(self.page_size, self.total_items - offset) 21: return Page(offset=offset, item_count=item_count)

You open paginator.py and read lines 12–22:

def page(self, page_number):
    offset = page_number * self.page_size
    item_count = min(self.page_size, self.total_items - offset)
    return Page(offset=offset, item_count=item_count)

Step 4: Hypothesise

You write down: "28 items, page_size=10, page_number=3. Expected offset = 3 * 10 = 30. But 30 ≥ 28, so item_count = min(10, 28 - 30) = min(10, -2). That's negative — something's wrong with the negative case. But the test got 10, not a negative. Let me check whether min() of a negative and a positive returns the positive."

You verify: min(10, -2) returns -2 in Python. But the test got 10. That means the min() isn't being reached for negative values, or the offset is different. You update your hypothesis: "Maybe the offset calculation is wrong — page_number=3 might be correct if pages are 0-indexed starting at page 1, not 0. Let me re-read the test."

The test comment says # 0-indexed: page 3 is the third page. Third page (0-indexed from 0) should be pages 0, 1, 2. Page 2 is the last page. Calling p.page(3) is requesting a non-existent page. But the test says "page 3 is the third page" — suggesting pages are 1-indexed in the test's intention, while paginator.py uses 0-indexing.

Step 5: Use the debugger

You set a breakpoint inside page() and run the test in debug mode. You inspect the values:

→ breakpoint: paginator.py:13 page_number = 3 offset = 30 self.total_items = 28 self.page_size = 10 → total_items - offset = -2 → min(10, -2) = -2 ← but test reported item_count == 10 # item_count is -2... so why does the assertion say "got 10"? # Step out and check: what does Page.__init__ do with a negative item_count?

You step into Page.__init__:

class Page:
    def __init__(self, offset, item_count):
        self.offset = offset
        self.item_count = item_count if item_count > 0 else self.page_size  # ← ???

There it is. Page was written with a "guard" that replaces a negative item_count with… self.page_size — but Page doesn't have a page_size attribute. In Python, that raises an AttributeError… except the constructor silently falls back to the attribute lookup of self.page_size, which returns the default value from object.__getattribute__... you check: Page inherits from a base class that sets self.page_size = 10 as a class attribute. So a negative item_count results in item_count = 10 — the page size — which is the wrong behavior and explains the test failure exactly.

Step 7: The fix

Two changes are needed: the guard in Page should clamp to zero (not silently return a full page), and paginator.py should guard against returning a negative item count in the first place.

# paginator.py — fix the negative item_count at the source
def page(self, page_number):
    offset = page_number * self.page_size
    items_remaining = self.total_items - offset
    item_count = max(0, min(self.page_size, items_remaining))  # max(0,...) prevents negative
    return Page(offset=offset, item_count=item_count)

# Page.__init__ — remove the broken guard
def __init__(self, offset, item_count):
    self.offset = offset
    self.item_count = item_count  # item_count is now always ≥ 0; no guard needed

Step 8: Re-run tests

python -m pytest tests/test_paginator.py -v test_first_page_full .............. PASSED test_middle_page_full ............. PASSED test_last_page_is_complete ........ PASSED test_single_page .................. PASSED test_empty_collection ............. PASSED 5 passed in 0.08s

All tests pass. You narrate the full root cause to the interviewer: "The bug was in two places — paginator.py didn't guard against a negative item_count when the offset exceeds the total, and Page.__init__ had a broken fallback that substituted self.page_size (a class attribute it accidentally inherited) when the count was negative. I fixed the root cause in paginator.py with a max(0, ...) guard, and removed the broken guard from Page.__init__."

✅ The narration is part of the answer

Notice that the worked walkthrough above includes a moment where the first hypothesis was wrong — the offset calculation wasn't wrong, but the Page constructor had a broken fallback. Saying "my first hypothesis was incomplete — here's what the debugger showed me and why I updated it" is not a weakness in this interview. It's exactly the signal the interviewer is looking for: a candidate who updates their model based on evidence rather than doubling down on a wrong guess.

The Integration round: working with an unfamiliar API

The Integration round gives you a working starter project and asks you to extend it. The starter code may already have a few working endpoints and some authentication wired up — your job is to add a specific piece: handle a webhook event, add an idempotency key to a charge, implement retry logic.

The same habits apply — read before touching, reproduce (run the existing tests to confirm the baseline is green), then work incrementally. But there are some specific skills the Integration round tests that Bug Squash does not:

Common mistakes in Bug Squash rounds

⚠️ Mistake 1: Jumping to a fix without reproducing

The most frequent failure mode. A candidate reads the issue description, thinks "I see it," and immediately starts editing code — without running the failing test first. If the test was not actually failing in the environment they're in (different Node version, a missing env variable, etc.), they'll "fix" something that was never broken and potentially break other things. Always reproduce first.

⚠️ Mistake 2: Silent debugging

Spending 20 minutes in silence, staring at code or running tests, while the interviewer watches. They can't tell whether you're stuck, whether you've formed a hypothesis, or whether you're about to find the bug. Keep narrating. Even "I'm reading through the class hierarchy to find where this value is set" is better than silence. Silent debugging is also a professional problem — your colleagues can't help you if they don't know what you're investigating.

⚠️ Mistake 3: Rabbit-holing

Following an interesting-looking path that doesn't connect to the failing test. You find a potential issue in a helper function, spend 15 minutes understanding it, and eventually realize it's unrelated to the failure. The fix: always trace from the failing assertion backwards. Every minute of investigation should be in service of explaining the specific value or behavior that the test says is wrong.

⚠️ Mistake 4: Print-only debugging

Scattering console.log or print() statements throughout the code. It works for simple cases, but it means you can only see the variables you thought to print — and in a timed interview, re-running the test after each print statement wastes time. Use your IDE's debugger: set one breakpoint at the entry point of the suspicious function, then step through. You'll see every variable, not just the ones you guessed.

⚠️ Mistake 5: Not running the full test suite after your fix

Making the originally failing test pass and declaring victory. Your fix might have changed behavior that other tests depend on. Run the full suite. If you break tests you weren't aware of, that's important information — either your fix was too broad, or there are multiple bugs and the other tests were already broken. Mention it either way.

How to prepare

Practice with real open-source repos

The closest practice to the actual Bug Squash is to find a real open-source library in your primary language and work through its open bug reports. Clone the repo, check out a version where a reported bug is present, run the failing test, and fix it. Don't read the fix in the PR — work through it yourself first. Libraries with good test suites and labelled issues (GitHub issues tagged "bug" with a linked failing test) are ideal. After you fix it, compare your fix to the merged PR. Notice where they diverge and why.

Learn your IDE's debugger cold

Before the interview, be comfortable with these operations in your IDE without looking them up:

These operations should feel automatic. If you're looking up how to set a conditional breakpoint mid-interview, you're spending attention on the tool instead of the problem.

Time-box your investigation steps

Practice working under a timer. Give yourself 2 minutes to orient (find the relevant file), 3 minutes to form a hypothesis, 5 minutes to validate it with the debugger. These time-boxes are not rigid, but having them in mind prevents you from spending 20 minutes on orientation because you read every file instead of following the call stack.

Practice narrating while debugging

This is a specific skill that feels unnatural at first. Practice it explicitly: debug something you know, and narrate every step out loud as if you're in an interview. "I'm setting a breakpoint here because this is where the value first gets computed. I'm running the test in debug mode. The breakpoint hit — I can see the value is 7 when I expected 5. That confirms my hypothesis. Now I'll look at where this value is assigned." Recording yourself and listening back is uncomfortable but effective.

Under the hood: a second worked bug — a state/ordering bug found cold

The OffsetPaginator walkthrough above showed an off-by-one in a small, self-contained module. This second worked walkthrough shows a different category of bug that is much harder to spot by reading: a wrong-default combined with a state ordering problem in a billing library you've never seen. This kind of bug produces correct behavior in tests but wrong behavior in production because it depends on the order that callers configure an object.

Setup

You've been handed a Node.js repo, invoice-builder, with the instruction: "One test is failing — test_tax_exempt_invoice_has_no_tax. Find and fix the bug." The repo has an Invoice class, a TaxCalculator helper, and 12 tests. You've never seen it before. Time limit: 45 minutes.

Step 1–2: Read the test, reproduce

npm test -- --grep "tax_exempt" 1 failing 1) Invoice tax_exempt_invoice_has_no_tax AssertionError: expected 120 to equal 100 at Context.<anonymous> (test/invoice.test.js:44:5) at processTicksAndRejections # Confirmed. A tax-exempt invoice for $100 is producing $120 — 20% tax is being applied # even though the invoice is flagged as tax-exempt. Expected: 100. Got: 120.

You read the full test:

// test/invoice.test.js:38–45
it('tax_exempt_invoice_has_no_tax', function() {
  const inv = new Invoice();
  inv.setTaxExempt(true);      // mark exempt BEFORE adding items
  inv.addItem({ price: 100, qty: 1 });
  const total = inv.finalize();
  assert.strictEqual(total, 100); // should be 100, no tax
});

Step 3: Orient — follow the call stack

grep -n "finalize\|setTaxExempt\|taxRate\|exempt" src/invoice.js 8: this.taxRate = new TaxCalculator().defaultRate(); // ← set in constructor 14: setTaxExempt(flag) { this.exempt = flag; } 22: finalize() { 23: const subtotal = this.items.reduce((s, i) => s + i.price * i.qty, 0); 24: const tax = this.exempt ? 0 : subtotal * this.taxRate; 25: return subtotal + tax; 26: } # Logic at line 24 looks correct: if exempt, tax = 0. Let me check TaxCalculator.defaultRate() grep -n "defaultRate\|rate\|exempt" src/tax-calculator.js 4: defaultRate() { return this.rate; } 7: constructor() { 8: this.rate = 0.20; // default: 20% 9: if (global.TAX_CONFIG) { 10: this.rate = global.TAX_CONFIG.rate; 11: } 12: }

Step 4: Hypothesise

The finalize() logic at line 24 looks correct. You write down: "If this.exempt is true when finalize() is called, tax should be zero. The logic is right — so either setTaxExempt(true) isn't actually setting this.exempt to true, or something is resetting it. Let me inspect at runtime."

Step 5: Use the debugger — set breakpoint in finalize()

→ breakpoint hit: invoice.js:22 (finalize) this.exempt = false ← should be true — setTaxExempt(true) was called! this.taxRate = 0.20 this.items = [{ price: 100, qty: 1 }] # this.exempt is false even though setTaxExempt(true) was called before addItem(). # Step out — set a second breakpoint on setTaxExempt() to confirm it was called. → breakpoint hit: invoice.js:14 (setTaxExempt) flag = true → after assignment: this.exempt = true ← exempt is true here # addItem() is called next. Set breakpoint there. → breakpoint hit: invoice.js:17 (addItem) item = { price: 100, qty: 1 } → after addItem: this.exempt = false ← addItem() reset exempt to false! # addItem() is overwriting this.exempt. That is the bug. Let me read addItem().

You look at addItem():

// src/invoice.js:16–20
addItem(item) {
  Object.assign(this, {
    items: [...this.items, item],
    exempt: false,         // ← BUG: hardcoded false, overrides setTaxExempt
    taxRate: this.taxRate   // (unnecessary but harmless)
  });
}

Root cause found. A developer added exempt: false to the Object.assign call — probably copying from a reset/initializer pattern — without realising it would overwrite any prior call to setTaxExempt(true). This is a wrong-default state ordering bug: the code works correctly if addItem() is never called after setTaxExempt(true), or if you call setTaxExempt(true) after all items are added. The test calls them in the natural order (set exempt, then add items), which exposes the bug.

The fix — smallest change that addresses the root cause

// src/invoice.js:16–20 — remove the exempt override from addItem()
addItem(item) {
  this.items = [...this.items, item];
  // do not touch this.exempt — it is set independently by setTaxExempt()
}

No refactoring. One line removed. The fix addresses exactly the diagnosed cause: addItem() should only update this.items.

Re-run — all tests green

npm test 12 passing (0.4s) # tax_exempt_invoice_has_no_tax: PASSED # All other tests: PASSED # Full suite green. No regressions.

You narrate to the interviewer: "The bug was in addItem() — it used Object.assign with a hardcoded exempt: false, which overwrote any prior call to setTaxExempt(true). The logic in finalize() was correct; the state was wrong by the time finalize() ran. The fix is to remove the exempt property from addItem() entirely — that method's job is to append an item, not to manage the exemption flag."

✅ What this bug class looks like in production

State-ordering bugs are particularly dangerous because they typically pass all tests written by the same developer — that developer naturally calls methods in the order that works. They're exposed by a different caller using a different order, by a framework that initialises objects in a different sequence, or by a test written to verify a specific API contract. The debugger is essential here: reading the code, the finalize() logic looks correct. Only stepping through at runtime reveals that this.exempt was already false by the time it was checked.

🧠 Quick check

1. In a Bug Squash round, what matters more than finding the bug?

Interviewers score the process, not just the outcome. A candidate who clearly diagnoses the root cause — reproduces, hypothesises, uses the debugger, narrates their reasoning — but doesn't quite finish the fix in 45 minutes can score higher than a candidate who silently fumbles to the correct fix through random changes.

2. You run the failing test and get: AssertionError: expected 42 to equal 40. What should you do before reading any production code?

Start at the test, trace to the production code. Reading top-to-bottom is orientation without direction — you'll spend time on unrelated code. Searching for the literal value 42 is fragile and unlikely to point to the logic bug. Follow the call stack from the test to the suspect function.

3. Why use an IDE debugger with breakpoints rather than print statements?

Print statements require you to predict which variables to print before you run. A debugger lets you stop anywhere and explore anything — step into a function you didn't anticipate, inspect the call stack, set a watch on a value you didn't know existed when you started. In a timed interview with an unfamiliar codebase, that flexibility is a significant advantage.

4. After your fix makes the originally failing test pass, you run the full test suite and find two other tests now failing. What should you do?

Newly failing tests are important information. Investigate: did your fix change shared behavior? Are these tests covering a related but distinct case the bug also affected? Narrate your findings to the interviewer — this kind of discovery is exactly what the round is designed to surface.

✍️ Exercise A: your first five moves in an unknown repo

You're dropped into an unknown repository with a failing test. You have no context beyond the test output: FAILED tests/test_billing.py::test_invoice_total — AssertionError: assert 150.0 == 125.0. Write out your first five moves, in order, with a sentence explaining what each move tells you.

Model answer:

  1. Read the full test output. Look at the stack trace, not just the assertion line. The stack trace shows every function on the call path — that's your map into the production code. You learn: which class/function the test is calling, how many layers deep the computation goes, and whether there's an intermediate calculation that might be wrong.
  2. Open the test file and read the test. What is the test setting up? What inputs does it pass? What does 125.0 represent and why is 150.0 wrong? Often the test comment or variable names explain the business logic ("total should exclude tax for exempt customers").
  3. Navigate to the production function the test calls. Use "go to definition" from the test call site. Don't browse the repo structure — jump directly to the relevant code.
  4. Read only the function body and any helper functions it calls. Form a hypothesis: where could a 25-unit discrepancy come from? A fixed fee being applied when it shouldn't? A tax rate that's wrong? A discount not being subtracted?
  5. Set a debugger breakpoint at the entry of the production function and run the test in debug mode. Step through and watch values change. Your hypothesis tells you what to look for; the debugger tells you whether you're right.

Rubric: ✓ First move is reading the test output, not opening code ✓ Test file comes before production code ✓ Navigation is targeted (go-to-def), not browsing ✓ Hypothesis is formed before the debugger is opened ✓ Debugger is used, not print statements.

✍️ Exercise B: diagnose the mini bug

You're given the following JavaScript function and a failing test. Diagnose the bug without running the code — write your hypothesis and the fix.

// src/discount.js
function applyDiscount(price, discountPercent) {
  const discount = price * discountPercent;  // e.g. 100 * 20 = 2000
  return price - discount;
}

// tests/test_discount.js — failing
expect(applyDiscount(100, 20)).toBe(80);
// AssertionError: expected 80, got -1900

Model answer:

Hypothesis: The discount is being calculated as price * discountPercent where discountPercent is passed as a whole number (20) rather than a fraction (0.20). 100 * 20 = 2000, so price - discount = 100 - 2000 = -1900. The function treats the input as if it's already a decimal, but the test passes it as a percentage.

Fix option A — normalize the input inside the function:

function applyDiscount(price, discountPercent) {
  const discount = price * (discountPercent / 100);  // 100 * (20/100) = 20
  return price - discount;  // 100 - 20 = 80 ✓
}

Fix option B — rename the parameter and update all call sites to pass a decimal:

function applyDiscount(price, discountFraction) {  // expects 0.20, not 20
  return price - (price * discountFraction);
}

Rubric: ✓ Correctly identifies the percent-vs-fraction confusion ✓ Traces the arithmetic to show how -1900 is produced ✓ Proposes a fix (either option is valid) ✓ Notes that Option B requires updating call sites — awareness of scope of change.

Key takeaways

Sources & further reading