Where Types Stop Catching Bugs
Why Stricter Types Don't Get You to Zero Bugs
You turn on strict, ban any, and push type coverage up. Bugs still ship. The common complaint is "we tightened the types and the bug count didn't move" — and that's a sign the role of types is being misjudged. Types only guarantee shape consistency that's knowable at compile time. They say nothing about what actually happens at runtime.
Start by splitting bugs into two buckets: the ones types eliminate, and the ones they don't.
What Types Do and Don't Catch
Bugs types catch
- Typo'd property names (
user.nmae) - Missing or mismatched function arguments
- Unhandled null / undefined (with
strictNullCheckson) - Call sites left stale after a refactor
A compiler catches these mechanically. Investing in stricter types pays off here, reliably.
Bugs types don't catch
- Values crossing a boundary — API responses, form input, localStorage, callbacks from third-party SDKs. A type annotation is a declaration of what you expect, not a guarantee of what actually arrives at runtime
- Logic errors — the types line up, but the calculation or branching itself is wrong (discount order of operations, rounding drift, an off-by-one between
<and<=) - State and timing errors — async completion order, race conditions, double submits. Type systems don't reason about "when"
Dividing the Work: Types, Tests, and Runtime Validation
These three aren't competing tools — they cover different failure modes.
- Types: eliminate shape errors the compiler can see. Strong against typos and mechanical mix-ups
- Tests: guarantee the logic is correct. Only a test catches a calculation that's wrong even though the types check out
- Runtime validation: guards the boundary types can't reach. Validate API responses and form input the moment they arrive, before trusting the type
Where to Put Runtime Validation at the Boundary
A common failure pattern: slapping a type annotation on an API client's return value and calling it done.
// anti-pattern: a type annotation isn't validation
const res = await fetch("/api/v1/bug_reports/1");
const data: BugReport = await res.json(); // nothing is actually checked at runtime
This gives you a type, not a guarantee — it won't catch a server returning a stale shape or a field coming back null. Use a schema validation library like zod to enforce the boundary explicitly.
const BugReportSchema = z.object({
id: z.number(),
status: z.enum(["open", "in_progress", "closed"]),
screenshotPath: z.string(),
});
const res = await fetch("/api/v1/bug_reports/1");
const parsed = BugReportSchema.safeParse(await res.json());
if (!parsed.success) {
// this is the first point where a runtime shape violation is actually detected
reportSchemaViolation(parsed.error);
throw new Error("unexpected bug_reports response shape");
}
Don't validate everything — that gets expensive fast. Reserve schema validation for values that cross in from outside your own code: APIs, forms, storage, third-party SDKs.
Checklist: Can I Trust This Value's Type?
- Where did this value come from — a computation inside your own code, or something external (API, form, storage, SDK)?
- If external, is it validated the instant it arrives, or are you relying on a type annotation alone?
- The types check out — but is a test confirming the calculation or business logic is actually correct?
- Does the design survive async operations completing out of order? Types can't tell you this either way
Putting This Into Practice with Bugoon
Boundary validation gaps and state-timing bugs tend to slip past type checks and unit tests and only surface in production. When a bug is reported through Bugoon's browser widget, a screenshot, the interaction steps, and the Console / Network error log are attached automatically — so even in a "the types checked out but it broke at runtime" case, the report carries a trace of what value actually arrived. Reports convert directly into GitHub Issues, and the MCP server can hand the whole reproduction context to Claude Code or Cursor, making it easier to pin down exactly where an unexpected value crossed the boundary.
Streamline bug reporting for your team.
Bugoon is free to get started. Add one line of code to your site and transform how your team handles bugs.
Get Started