04 โ QA & Reliability
Goal: eradicate "silly bugs," pass QA cleanly, and reach the state where your code is trusted on sight.
The core principle: Silly bugs are not random โ they are systematic, and systems kill them. Every silly bug you've ever shipped came from one of: an unenumerated state, an untyped boundary, an untested edge, or a self-review you rushed. Fix the four causes and the bugs die with them.
Part 1 โ The self-audit protocol (before you say "done")
1.1 The pre-PR gate (run every time, in order โ ~15 min)
- Diff-as-stranger pass: re-read your own diff as someone who has never seen the ticket. Would the why survive without you? If a hunk looks mysterious to you now, it will look criminal to a reviewer in 6 months.
- The adversarial pass: ask "how would this break?" โ then the three variants: empty input / rapid repeated action / mid-flight navigation. If you can't answer for all three, the code isn't done.
- State enumeration check: list every UI state the change can be in (loading / empty / error / success / offline / stale-while-refetching). Is each handled, visibly? An unhandled state is a bug waiting for a user to find it.
- The delete-test: delete the code you just wrote (in your head). What breaks? If nothing obvious breaks, the code may be doing less than you think โ or nothing at all.
- The 10-minute rule: if you can't explain the change in 10 lines to a colleague, you don't understand it well enough to merge it. Rewrite until you can.
1.2 The fresh-eyes pass (the single highest-value habit)
Never merge the same hour you wrote it. A 20-minute gap (lunch, another task, a walk) resets your brain's familiarity blind spot โ you will catch typos, dead code, and wrong variable names that were invisible minutes earlier. For large changes: sleep on it; review first thing next morning with coffee before any other code.
1.3 The "QA-proof" handoff
Give QA the ammunition to find real bugs instead of rediscovering yours:
- PR description includes: what changed, the state/edge checklist (1.1.3) you already ran, known limitations, and the test scenarios from the plan (03).
- QA's first question ("what did you test?") has a written answer. QA that trusts your handoff spends its time on deep edge cases, not your happy path โ that's the relationship that makes releases clean.
Part 2 โ The silly-bug eradicators (systemic, one-time investments)
| Eradicator | Kills | Setup |
|---|---|---|
Strict TS, zero any (noUncheckedIndexedAccess, exactOptionalPropertyTypes) |
undefined crashes, shape drift |
One-time config (02, Step 4); CI-enforced |
| Generated API client from OpenAPI | Handwritten type drift, wrong field names | Contract-first pipeline (02, Step 3) |
| Runtime validation at the boundary (zod on server-action inputs / route handlers) | Garbage-in from forms, API payloads | Validate once at the edge; trust inward |
| Error boundaries per route/feature | White-screen-of-death for the whole app | React error boundaries with a fallback + report |
| Typed query keys (factory functions) | Stale data because a key string was mistyped | queryKeys.feature.detail(id) |
| Race-condition hygiene (AbortController / ignore-flag in effects; stale-closure lint) | Old responses overwriting new ones; update loops | useEffect cleanup always; @tanstack/query handles its own |
| State-machine thinking for complex UI (enumerate states + transitions explicitly, no boolean soup) | Impossible states ("loading AND error") | Type the state: type ViewState = {status:'loading'} | {status:'error',e} | {status:'ready',data} |
| Double-submit & idempotency guards | Duplicate orders/toasts/requests | Disable-while-pending; idempotency keys on mutations |
| The empty/error/offline trio as a component standard | Skeleton-less loading, silent failures | Design-system states (02, Step 6) |
Part 3 โ The extreme edge-case checklist (run before "ready for QA")
3.1 Input & data edges
- Empty array / null /
undefinedfrom every fetch โ rendered gracefully? - Whitespace-only / unicode / emoji / RTL text / extremely long strings (100k chars)?
- Huge data volumes โ list virtualization or pagination? (50k rows kills naive renders)
- Numbers:
0, negatives, decimals,NaN,Infinity, very large (float precision), timezone offsets? - Dates across DST boundaries and timezones (ISO-8601 with
Zeverywhere; format at the edge)? - File upload: empty file, huge file, wrong type, upload interrupted, retried?
3.2 Network & lifecycle edges
- Loading state on slow network (3G throttle) โ skeleton, not blank?
- Error state per request type (network down โ 500 โ 404 โ rate-limit) โ distinct, actionable messages?
- Race: user navigates away mid-request โ no setState on unmounted component, no flash of stale data on return?
- Race: rapid filter changes โ does the last response, not the last request, win?
- Double-submit: button spam, Enter key in forms, retry-after-timeout โ exactly one mutation?
- Idempotency: retried POST does not duplicate the resource
- Offline: service worker / offline state handled; reconnection reconciles?
- Back/forward cache (bfcache): page restored from cache is consistent?
3.3 Interaction & accessibility edges
- Keyboard-only: tab order sensible, focus visible, modals trap + restore focus, Esc closes
- Screen reader: labels present, live regions announce async updates, no aria misuse
-
prefers-reduced-motionrespected - Touch: tap targets โฅ 44px, no hover-only interactions, no 300ms lag
- Zoom 200%: layout doesn't break or clip
- Rapid interaction during loading: clicking a disabled-but-rendering button, submitting mid-refetch
- Text selection / paste into controlled inputs behaves
- Browser back/forward with client-side routing: state, scroll, and URL agree
3.4 The senior edge (what actually ships bugs at 10 YOE)
- The feature flag off-path: code behind the flag is still exercised in CI (flagged-off tests) โ flags rot silently
- The 2nd render: is state initialized correctly on remount (route param change reusing the component)?
- The empty state of your own abstraction: the utility you wrote for one case, called with the case you didn't imagine
- The silent catch: a
catchthat swallows and pretends success (log it, surface it, or don't catch) - The "temporary" code: TODOs older than the sprint โ either done or tracked with an owner
Part 4 โ The testing tiers (make QA an echo, not a discovery)
| Tier | Catches | Volume rule | Tooling (modern) |
|---|---|---|---|
| Unit (pure logic, reducers, utils) | Logic bugs | Dense on pure code | Vitest |
| Integration (component + behavior) | State, events, a11y regressions | Per component, user-flow-shaped | React Testing Library + Vitest |
| E2E (critical journeys only) | Wiring across the system | โค 10 journeys, kept green | Playwright |
| Property-based (pure functions) | Edge inputs humans never imagine | Where logic is complex | fast-check |
The 3 rules that keep tests alive (not decorative):
- Test behavior, not implementation โ assert what the user sees/experiences, so refactors don't break tests.
- Every bug fix ships with its regression test โ a bug without a test is a bug that will return. Non-negotiable, including "silly" ones (those are the ones that return most).
- Red-green discipline: the test must fail before the fix (prove it catches the bug), then pass after. A test that never failed proves nothing.
AI-assisted test generation: prompt Claude Code/Copilot with the component + your edge list โ "generate tests for these states: [list]" โ then read every generated test as your own spec. Generated tests that encode the wrong behavior are worse than no tests.
Part 5 โ The pre-merge gate (the final 5 minutes)
- Pre-PR gate passed (1.1) + fresh-eyes pass done (1.2)
- Strict TS + lint clean in CI (no suppressions without a comment + ticket)
- New behavior has tests; tests were red before the fix
- Edge checklist (3.1โ3.4) walked for this change's blast radius
- Error paths are visible (not silent catches); loading/empty/error states present
- Race conditions considered for async work (cleanup, latest-wins)
- No debug logs / commented code / TODO without owner
- QA handoff written (states tested, known limits)
The confidence definition: you merge with the same calm you'd have demoing the feature to a room of stakeholders on a bad network, live. If you wouldn't demo it, it isn't done โ go back to Part 1.
โ Next: 05 โ Career Accelerator ยท Back: 03 โ Flawless Execution