Step 5 — Failure Lab
Goal
Diagnose three seeded failures from evidence, before reading any explanation. Two of them are realistic production incidents; one is on the specification's failure case-study list.
Prerequisites
- Steps 1–4 complete
Setup
cd src
npm run serve # http://localhost:8080/lab.html
Open the console and load the bugs:
await import('./bugs.js');
Do not read the notes at the bottom of web/bugs.js until your written diagnosis is done.
F1 — The frozen tab
Bugs.f1()
A "progress-friendly" chunked implementation using recursive queueMicrotask. The tab freezes.
No error, no crash dialog, CPU at 100 %.
Predict before running:
- Does the rAF-driven counter stop?
- Can you get a useful Performance recording?
- Can you break in with the debugger?
- Does the tab recover on its own?
Then run it and explain precisely why "chunking" changed nothing. You already have the mechanism from Step 2; this step is about recognising the symptom without knowing the cause in advance.
Corresponds to: specification failure case study "rendering loop freezes browser."
F2 — The stale overwrite
Bugs.f2()
A debounced search against an endpoint with randomised 50–800 ms latency. Typing a query then correcting it intermittently leaves the results showing the old query's results — roughly 1 run in 6.
This is the specification's "stale response overwrites newer data" incident.
Your tasks
-
Reproduce it deterministically.
Bugs._f2.fakeFetch.forceOrder([300, 60])forces the first request slow and the second fast, guaranteeing out-of-order completion.If your reproduction is "type fast a few times," you have observed it, not reproduced it. The difference is the difference between a fix and a hope — and it is the difference between a regression test that holds and one that flakes.
-
Name the violated invariant in one sentence.
-
Implement three fixes:
AbortControllercancellation- a request sequence number (last-write-wins on issue order)
- a render-time guard comparing the response's query against current state
-
Rank them and defend the ranking. All three "work" for a read.
-
Which is correct when the request has a server-side effect? This question is the seam into
fe-17(races, cancellation, idempotency) andfe-18(API integration as a distributed system). A partial answer is fine here; a stated partial answer is the point.
Note on the debounce
The debounce reduced the number of in-flight requests. Establish for yourself what it did to the ordering guarantee. This is the trap the incident depends on.
F3 — The invisible progress bar
Bugs.f3()
A loop updating progressBar.style.width on every iteration of a 5,000-item job. The bar jumps
0 % → 100 % with nothing in between, and the whole job is slower than the version with no
progress bar at all.
Two symptoms. Two different causes. Explain both, then fix it so the bar animates smoothly and the job finishes faster than the original.
You have both mechanisms already — one from Step 2, one from Step 3. This step tests whether you can select the right one from a symptom rather than recognise it from a heading.
Debugging exercise
For each failure, produce written answers to the specification's incident protocol:
- What do you investigate first?
- What evidence do you need?
- What hypotheses exist?
- What experiments distinguish them?
- What mitigation is appropriate?
- What permanent fix is appropriate?
- What systemic change prevents recurrence?
Question 7 is the Principal-level one, and it is where most incident reviews stop short. For F2,
"we added a guard" is a fix; "our data-fetching layer makes unordered responses unrepresentable,
and the lint rule catches raw fetch in components" is a systemic change.
Testing considerations
Turn F2 into a regression test that cannot flake:
await page.route('**/search*', async (route) => {
const q = new URL(route.request().url()).searchParams.get('q');
await new Promise(r => setTimeout(r, q === 'nov' ? 300 : 60)); // force N after N+1
await route.fulfill({ json: { query: q, results: [`results for "${q}"`] } });
});
Controlling response ordering explicitly converts a probabilistic bug into a deterministic test.
That conversion is the whole game, and it recurs throughout fe-33.
Note also, from Step 1: assert on counted invariants (requests issued per N keystrokes; final rendered state matches the last query), never on wall-clock durations. Shared CI runners have order-of-magnitude variance, and a duration assertion will be deleted within a month — correctly.
Checkpoint
Module completion in docs/verification.md requires F1–F3 diagnosed from evidence, F2 reproduced
deterministically, and the three F2 fixes ranked.