Expected, Actual, Which Operation, Which Branch, Which Assumption
A wrong cart is not "broken"; it is a specific expected state, a specific actual state, one operation that turned the first into the second, one branch inside it that ran when another should have, and one assumption that made you write it that way. Five questions, in that order, and the bug has nowhere to hide.
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.
The cart shows Laptop twice. You have one function on the screen. What are the five questions that take you from "it is wrong" to the line, and why does the order of them matter?
You ran the script — add Laptop, add Laptop, view — and the view shows two Laptop rows with quantity 1 each. The tests you wrote pass. You have addItem, removeItem and the view function open in three tabs and you are reading all of them at once.
Read the code looking for the bug. Start at addItem, scan for anything suspicious, add a log statement, run again, see two rows again, scan removeItem, wonder whether the view is duplicating, add a log there. Each step feels like narrowing; it is actually widening, because nothing has been excluded.
Reading for "anything suspicious" has no stopping rule. Every line could be the bug until something says it is not, and staring does not say it; the search ends when you are tired, not when the bug is found.
- Reading for "anything suspicious" has no stopping rule. Every line could be the bug until something says it is not, and staring does not say it; the search ends when you are tired, not when the bug is found.
- The tests pass, which is not evidence the code is right — it is evidence the tests do not contain this case. Nothing in the reflex asks which case is missing, so the same bug will come back after the fix, from a different script.
- Three tabs, one bug. The cart is a sequence of operations on one state; the bug is in exactly one of them, and the reflex has not asked which. Logging everywhere produces the whole trace of everything, in which the wrong stop is one line among fifty.
The move
Precisely enough to apply it to a problem you have never seen — not a slogan.
- Expected state: write it, from the concept's examples, not from memory. After add Laptop twice the concept says [ Laptop × 2 ]. Actual state: write what you saw — [ Laptop × 1, Laptop × 1 ]. The gap between the two lines is the bug, described as data, before any code is opened.
- Which operation changed it: walk the script, comparing state after each step to the expected state after each step (Before, Operation, After — and Exactly What Changed). After the first add, both say [ Laptop × 1 ]. After the second, they diverge. The second addItem is the operation; removeItem and the view are excluded, and their tabs close.
- Which branch executed: trace the second call through its five stops (Input → Lookup → Branch → Mutation → Output). Input: [ Laptop × 1 ], "laptop". Lookup: the find should return the Laptop entry. Branch: the append path ran — the actual state proves it. So either the find returned nothing, or the branch on its result is wrong. Two lines left.
- Which assumption was wrong: the line you wrote assumed something.
find((i) => i.id === productId)assumed the field is calledid; the entries haveproductId. The find compared undefined to "laptop", returned nothing, and the append path ran on every call. The fix is one word, and it is the last step, not the first.
Five questions, in order
The pipeline is the method as steps, with the way each step fails when skipped. The order is the point: each question is answerable only because the previous one narrowed the space, and each one excludes code the reflex would have read.
- 1Expected state
Write the state the concept's example says you should have: [ Laptop × 2 ].
fails by Writing what the code does instead of what the example says — then expected equals actual by construction and there is no bug to find.
- 2Actual state
Write the state you observed, as data: [ Laptop × 1, Laptop × 1 ]. The gap is the bug, described without code.
fails by Describing the symptom — "it shows Laptop twice" — instead of the state, so it is not comparable field by field.
- 3Which operation
Walk the script; find the first step after which expected and actual differ. That operation is the suspect. Close the other tabs.
fails by Reading every function at once; nothing is excluded and the search has no end.
- 4Which branch
Trace the suspect call through its five stops; the actual state says which path ran. Two lines remain.
fails by Adding logs everywhere and reading the whole trace of everything instead of the one stop that diverged.
- 5Which assumption
Say in one sentence what you believed when you wrote those lines — "the field is called id" — fix it, and add the exposing example as a test.
fails by Fixing the word and stopping; the assumption is usually made in two other places, and the test that would catch it is still missing.
The first two questions need no code open. The third closes most of it. Only the fourth and fifth read lines, and by then there are two of them.
Three bugs, one table
The failure table is the three worked bugs laid out as trigger, symptom, cause, response. The point of the table is the third column: all three causes are assumptions, stated as sentences, and each response ends with a test from the example that exposed it.
| Trigger | Symptom | Cause | Response |
|---|---|---|---|
| add Laptop twice | [ Laptop × 1, Laptop × 1 ] instead of [ Laptop × 2 ] | The find compared i.id to the product id; entries carry productId, so the find returned nothing and the append path ran every time. | Fix the field name; add the concept's add-again as a test, since its absence is why the suite was green. |
| changeQuantity to 0 | [ Laptop × 0 ] instead of [ ] | Assumed "set to zero is a quantity change"; the concept's rule makes it a removal, and the zero branch was missing. | Add the == 0 → removeItem branch; add to-zero as a test. |
| add Laptop twice, then reload | [ Laptop × 1 ] after reload; [ Laptop × 2 ] before | Assumed every mutation is saved; the save ran only on the code path that appended, not the one that increased. | The cart is correct — the persistence wrapper is the suspect; save after every operation returns, and add a save-then-load test with the changed list empty. |
What you do not know yet, made specific
Between the branch and the assumption there is sometimes a genuine unknown — you can see that the append path ran and not why the find returned nothing. The board turns that into a question with an experiment, which is the same move the discovery stage uses for a whole problem (Unknown to Specific Question) applied to two lines.
- ✓Expected [ Laptop × 2 ]; actual [ Laptop × 1, Laptop × 1 ]; the divergence is at the second addItem.
- ✓The append path ran, so the find returned nothing or the branch on its result is inverted.
- ~That the entries in cart.items have the shape the find expects — to be checked, not believed.
? The find is broken.
becomes Does
cart.items.find((i) => i.id === "laptop")return the entry when items is exactly [ { productId: "laptop", quantity: 1 } ]?experiment Call the find on that literal array in a scratch file; it returns undefined, and
i.idprinted inside the callback is undefined too.? Maybe it is the view merging things.
becomes Is the duplicate present in cart.items before the view runs, or only in what the view renders?
experiment Print cart.items.length after the second add: 2. The view is excluded; the duplicate is in the state.
Two experiments, each a line. The first names the assumption; the second closes a tab that was open for no reason.
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.
- Before opening any code, write two lines: expected and actual, as states, from the concept's examples. If you cannot write expected without looking at the code, the bug hunt is premature — the example is missing (Examples Before Algorithms).
- Bisect the script by state, not the code by lines: after which operation do expected and actual first differ? That one operation is the suspect; close everything else (Binary Search Over the System).
- Trace the suspect call with real values and find the stop where the trace stops matching what the code should have done. The branch line usually says which path ran; the actual state confirms it.
- Name the assumption in one sentence — "I assumed the field was called id" — and write the test that would have caught it, from the example that exposed it. The fix without the test is the bug scheduled to return (Examples Become Tests).
- If the failure is in a system rather than a function — a request, a database, a queue — the same five questions apply with bigger units, and the systems version of this lesson is Debugging Is Problem Solving.
Worked on a concrete problem
The move has to produce something. This is what it produced.
- The duplicate Laptop. Expected [ Laptop × 2 ], actual [ Laptop × 1, Laptop × 1 ]. Divergence after the second add; suspect: addItem. Trace: lookup should find the entry, branch took append — so the find returned nothing. Assumption: field name. Fix:
i.productId. Test: the concept's add-again, which was somehow not in the suite, is now in it. - A second bug, same method. Expected after changeQuantity("laptop", 0): [ ]. Actual: [ Laptop × 0 ]. Divergence at changeQuantity. Trace: the zero branch should delegate to removeItem; the actual state shows the assignment path ran. Reading only that branch: the check was
quantity < 0followed by the assignment, with no== 0case. Assumption: "setting to zero is just a quantity change". The concept's rule says it is a removal; the to-zero example becomes a test. - A third, where the suspect is not the function. Expected [ Laptop × 2 ] after two adds; actual [ Laptop × 2 ] in memory, [ Laptop × 1 ] after a reload. Divergence at the reload — not at any cart operation. The operation that changed the state is "save then load", and the trace shows the save ran after the first add and not the second. The assumption was "every mutation is saved"; the cart is right and the persistence wrapper is the bug (The Cart Disappears).
How you know it worked
What now exists that did not before, and what question you can now ask.
- Two lines — expected and actual, as states — exist before any code is opened.
- Exactly one operation is the suspect, and you can say what excluded the others.
- The branch that ran is named, and the actual state is the evidence for it.
- The assumption is one sentence, the fix is small, and a test from the exposing example was added before the fix was called done.
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.
- ?What state did the concept say I should have, and what state do I actually have — written as data?
- ?After which operation in the script do those two first differ?
- ?Which branch of that operation ran, and what in the actual state proves it?
- ?What did I assume when I wrote that line, and where else did I assume it?
What can go wrong
- Starting at the assumption. Guessing "it is probably the find" and reading the find is the reflex with a hypothesis; the hypothesis is untested until expected, actual and the divergence point are written, and it is wrong often enough to cost more than the four questions it skipped.
- Writing expected from the code. "It should do what the code does" cannot expose a bug in the code; expected comes from the concept's examples and rules, which is what they are for.
- Fixing without the test. The one-word fix works and the case that exposed it is still not in the suite; the next refactor reintroduces it and the five questions start again.
- Stopping at the branch. "The append path ran" is a symptom with a location; "because I assumed the field was called id" is the cause. The assumption is what generalises to the next bug — the same wrong assumption is usually in two other places.
- Writing expected and actual first costs a minute in which you are not reading code; for a typo you have already spotted it is a minute wasted.
- Bisecting by operation is exact and slow for long scripts; a debugger breakpoint on the suspect gets to the branch faster once the suspect is known — it does not find the suspect.
- Adding a test for every bug grows the suite by cases nobody predicted; that is the suite doing its job, and the alternative is a suite that only holds the cases you already imagined.
- "This is the same as the debugging module." That module debugs systems — requests, services, queues — with hypotheses and experiments. This lesson debugs one function with one state and five questions, which is what to do before the systems method is needed.
- "Step four is just 'read the code'." It is reading two lines with a specific question — why did this branch run? — after the other lines were excluded by evidence. Reading ten functions with no question is what the reflex was.
- "If the tests pass, it is not a bug in the function." The tests pass because the case is missing; the passing suite is a statement about the suite. The example that exposed the bug is the test that was missing.
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.
- GENERALExpected / actual / which operation / which branch / which assumption applies to any function that changes state, in any language; the units grow when the "function" is a request or a job, and the systems version is the debugging module.
- STAGE-SPECIFICWhile learning an operation, run all five questions on paper every time; once the operation is familiar, the first three happen in your head and the last two still deserve a written sentence and a test.
- ILLUSTRATIVEThe three bugs — the field name, the missing zero case, the unsaved second add — are invented to show the method; Laptop × 2 is the concept's own example, and the three tabs and the fifty log lines are for the shape of the argument.
Where the depth lives
This domain asks the question and hands the answer off by name.