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)

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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:


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

3.2 Network & lifecycle edges

3.3 Interaction & accessibility edges

3.4 The senior edge (what actually ships bugs at 10 YOE)


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):

  1. Test behavior, not implementation โ€” assert what the user sees/experiences, so refactors don't break tests.
  2. 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).
  3. 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)

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

โ† Flawless Execution