Predict the Bug
"Every addItem call inserts a new row." Before running it, say what happens when the same product is added twice. Duplicate entries — and the missing line is a rule, not a typo. Reading code for the example it would get wrong is how you review a cart you did not write.
The situation, the reflex, and why it stalls
Every lesson starts where being stuck starts: someone has a problem, and the first move that comes to mind feels like progress.
You are given a cart implementation you did not write. How do you find its bug from the examples, before running it — and how do you know the bug is a missing rule rather than a slip?
A teammate's addItem is three lines: push the item, return the cart. It is clean, it has a test, the test passes. You are asked to review it, and everything you can see is fine. You cannot point to a wrong line, because there is no wrong line — there is a missing one.
Read the code for mistakes: off-by-one, wrong variable, missing return. Find none, approve it. Reviewing means looking for errors in what is there.
The bug is an absence, and reading for presence cannot find it. Every line of the three is correct; the cart is still wrong on the second add.
- The bug is an absence, and reading for presence cannot find it. Every line of the three is correct; the cart is still wrong on the second add.
- The test that exists is the normal case — add a laptop, see a laptop — so it passes, and its passing is taken as evidence about a case it never exercised.
- When the duplicate is eventually reported, it gets fixed as a slip — a de-duplication in the view — instead of as the rule it is, and
changeQuantitystill cannot tell which of the two entries to change.
The move
Precisely enough to apply it to a problem you have never seen — not a slogan.
- Review with examples, not with eyes. Take the three kinds — normal, edge, invalid — for the operation and predict, from the code, what each would produce. The normal case will pass; it always does. The edge case is the one to predict carefully: what does this code do when the product is already there?
- Trace the edge example through the five stops. Input: laptop, on a cart that has a laptop. Lookup: is there one? In the buggy version there is no lookup stop — and a missing stop is the finding. Branch: none. Mutation: append. Output: two entries. You have predicted the bug without running anything.
- Name the bug as a rule, not as a line. "The find is missing" is a symptom; "the rule 'one entry per product' is not encoded" is the diagnosis, and it says what the fix has to be — a lookup and a branch, not a de-duplication somewhere downstream.
- Then run the example to confirm, and write it as the test that was missing. The review's output is not "add a find"; it is the add-again test, failing, with the rule named — after which the fix is obvious and its correctness checkable.
The code, and the example to predict
The buggy version from the concept record, and the question the record attaches to it. Read it once for slips — there are none — and then for the edge example: what happens when the same product is added twice? The answer is predictable from the code because the code has no lookup, and the prediction is the review.
Cart = [ Laptop × 1 ]
Cart = [ Laptop × 1, Laptop × 1 ]
1function addItem(cart, productId, quantity = 1) {2 cart.items.push({ productId, quantity })3 return cart4}Nothing here is wrong. Something is missing. Reading for errors in what is present cannot find it; predicting the edge example can.
The trace with a missing stop
Tracing the edge example through the five stops makes the omission visible as an empty stop. The correct version has a lookup that decides a branch; this version has neither, so every call takes the same path and the second add cannot behave differently from the first. The empty stop is the finding, and it names the rule that lived there.
- inputproductId = laptop, quantity = 1; cart = [ Laptop × 1 ]
- lookupMissing. Nothing looks for an existing entry with productId = laptop.
- branchMissing. With no lookup there is nothing to branch on; every call appends.
- mutationappend { laptop, 1 } to cart.items
- output[ Laptop × 1, Laptop × 1 ]. The rule "one entry per product" is not encoded; the fix is the lookup and the branch, not a filter somewhere downstream.
Why the fix must be the rule
A duplicate can be hidden in several places, and only one of the fixes is the rule. The table follows the same bug through the responses a reviewer might accept and shows what each leaves broken. Cart Lab ships this buggy variant; its invariant check catches the duplicate on the second add, and the exercise is to say which line is missing before the check says so.
| Trigger | Symptom | Cause | Response |
|---|---|---|---|
| De-duplicate in the view | The cart page looks right; changeQuantity still edits one of two entries | The symptom was fixed where it was seen, not where the rule lives | Reject: the state is still wrong, and every other reader needs the same filter. |
| Sum quantities in total | The total is right; the entries are still duplicated | Same — a reader compensating for a writer | Reject: total was already right by accident; nothing about the state improved. |
| Reject a second add as "already in cart" | No duplicates; a shopper who clicks twice gets an error | A rule was encoded — the wrong one; the example says increment | Reject: the add-again example fails; the rule chosen contradicts the decided after. |
| Find, then branch: increase or append | add-again passes; changeQuantity has one entry to change | The rule "one entry per product" encoded where the write happens | Accept, with the add-again test as the definition of done. |
The implementation ladder
Concept, examples, pseudocode, code, tests, production — for the concept this lesson is about. Code is the fourth tab, not the first.
Shopping Cart = A temporary collection of products the user intends to purchase, held between browsing and checkout.
- Does a cart have identity? Yes, weakly. Two carts with the same items are still two carts, because each belongs to someone and will become a different order. It needs an id once it leaves memory; in memory the variable is the identity.
- Who owns it? A shopper — a logged-in user or an anonymous session. The owner is part of the state because "my cart" has to be findable again.
- How long does it exist? From the first add until checkout or abandonment. Whether it survives a reload, a closed browser or a login is not a property of the concept; it is a persistence decision made later, and each answer changes where the cart lives.
- Should it survive reload? Usually yes for a store, usually no for a demo. V1 in memory says no; V2 browser storage says yes on one device; V3 server storage says yes everywhere the user is logged in.
- Should it survive login? Only if an anonymous cart and a logged-in cart are merged — a rule that does not exist in V1 and appears as a modification later.
- itemscollection of CartItemkeepThe cart is its items; without them nothing else means anything.
- items[].productIdidkeepThe reference to what is being bought. The catalog owns the product; the cart only points at it.
- items[].quantityinteger > 0keepTwo laptops is one entry with quantity 2, not two entries — the rule "one entry per product" needs a quantity to hold.
- owneruser id or session iddependsSo the cart can be found again by the person it belongs to.
- items[].productNamestringderiveIt would be convenient to render the cart without a catalog lookup.
- items[].pricemoneydependsThe total needs a price per item.
- totalmoneydropEvery screen shows the total.
- currencycodedependsPrices need a currency to be added.
- createdAttimestampdropAbandoned carts might be expired or emailed about.
- update Add item — the updated cart
- delete Remove item — the updated cart
- update Change quantity — the updated cart
- read View items — the list of entries — product id and quantity — for rendering
- domain Calculate total — the sum of price × quantity over the entries
- delete Clear cart — the empty cart
- • Every quantity is greater than zero.
- • One logical entry per product.
- • The total is never negative.
- • An unknown product cannot be added.
- • Quantity cannot exceed available stock — if inventory is enforced here.
How to do it
Most important first.
- For any operation you review, predict the edge example first; the normal case teaches nothing about a bug of omission (Normal, Edge, Invalid).
- Trace the edge through input, lookup, branch, mutation, output; an empty lookup or branch stop on an operation that needs one is the finding (Input → Lookup → Branch → Mutation → Output).
- State the bug as the rule that is not encoded, and check the other operations that rule touches —
changeQuantityon a duplicated cart has no right answer. - Deliver the review as a failing test from the example, named for the behaviour, so the fix has a definition of done (Examples Become Tests).
- Ask the author which example they wrote; if the answer is "the normal one", the missing rules are wherever the edges were not written (Why Is This a Map? — Detecting Cargo Cult and Routing Back).
Worked on a concrete problem
The move has to produce something. This is what it produced.
- The buggy version from the record:
cart.items.push({ productId, quantity }); return cart. Edge example: [Laptop × 1] + add Laptop. Prediction from the code: no lookup, so an append; the cart becomes [Laptop × 1, Laptop × 1]. Confirmed by running. Diagnosis: "one entry per product" is not encoded. - Consequences, predicted before running: the view shows Laptop twice; the total is right by accident (1 + 1 at the same price equals 2 at that price);
changeQuantity(laptop, 3)finds the first entry and sets it to 3 while the second stays at 1 — a cart of Laptop × 3 and Laptop × 1 showing a total nobody chose. The bug is small; its downstream is not. - The review: the add-again test, failing, with the note "fromExample: add-again; rule: one entry per product". The fix is the find and the branch. A de-duplication in the view would have made the picture right and left
changeQuantitybroken.
How you know it worked
What now exists that did not before, and what question you can now ask.
- You can name the example a piece of code gets wrong before running it, and the trace stop that is missing.
- The bug is stated as a rule and the fix is stated as a failing test.
- You can predict the downstream damage — the operation that has no right answer once the rule is broken — not just the immediate wrong state.
The questions you can now ask
The field this whole domain exists for. After this lesson, these are the questions to put to an unfamiliar problem.
- ?Which example would this code get wrong — and can I say so before running it?
- ?Which of the five trace stops is missing from this operation, and which rule lived there?
- ?Once this rule is broken, which other operation has no right answer?
- ?What failing test is this review, and what is its fromExample?
What can go wrong
- Every review becomes a hunt for missing rules and the actual slip —
<for<=— gets missed because it was present, not absent. Both readings are needed; this lesson is the one the reflex lacks. - The bug is predicted, confirmed, and fixed at the symptom — a filter in the view — because the diagnosis stopped at "duplicate" instead of reaching "rule not encoded".
- The prediction is made after running, and the review says "I knew it" about a bug the run found. The prediction has to precede the run to be a review skill rather than hindsight.
- Predicting the edge for every operation under review is slower than reading for slips, and most reviewed code has the find.
- Stating bugs as rules can sound pedantic to an author who wanted "add a find" — and the pedantry is where the
changeQuantityconsequence gets noticed. - A review delivered as a failing test asks the reviewer to write code, and some teams keep review and implementation apart on purpose.
- "So the bug was a missing find." The bug was a missing rule; the find is one encoding of it, and the map representation encodes the same rule with no find at all. Naming the line instead of the rule is what makes the view-side filter look like a fix.
- "Tests would have caught it." Only the add-again test would have, and it was not written because the example was not — tests catch what examples imagined. The lesson is upstream of the suite.
- "Clean code has no bugs." Falsifiable: the three-line version is the cleanest addItem in the record and the only wrong one. Cleanliness is a property of what is present; correctness includes what is absent.
Where this applies
Problem-solving advice is stated as universal far more often than it is. These labels say what each method is specific to — and where CONTESTED appears, the note gives the strongest form of the opposing view.
- GENERALPredicting the edge from the code is how to review any operation for a missing rule — a rate limiter without a window reset, a login without a lockout — and the trace with an empty stop is the same finding in each.
- TEAM-SPECIFICA solo learner uses this on their own past code and on received code; a reviewer on a team uses it as the first question of a review, and a team under deadline uses it only on the operations whose rules they know to be subtle — the second add, the last unit.
- ILLUSTRATIVEThe three-line buggy addItem, the teammate and the quantities are the concept record's invented predict-the-bug case; no real review is described.
Where the depth lives
This domain asks the question and hands the answer off by name.