Defence Before Fix: Converting Bug Reports into Permanent Defences
When you find a bug, build a static analysis rule that catches the entire class before fixing the specific instance. The DBF methodology by Joseph Edmonds converts reactive debugging into permanent, automated prevention.
When your team finds a bug, the instinct is to fix it. That instinct is incomplete. Fixing the bug is the last step. The first step is defending against the entire class of bug that just revealed itself.
This approach — Defence Before Fix, abbreviated DBF — is a methodology I developed after years of watching the same bugs reappear in new guises, new files, and new projects. A null-coalescing-to-empty-string pattern fixed in one function reappears three months later written by a different developer. An empty catch block removed in one service reappears in the next feature added by an AI assistant. The fix never propagated because nothing enforced the lesson.
DBF is now a published, versioned specification, maintained at defence-before-fix.github.io[src]. This article is my own introduction to it; the site is the definitive reference.
DBF changes that. Instead of fixing bugs one at a time, you encode the lesson into a static analysis rule — an automated check that catches every future instance of that pattern at the point of writing, not in production.
Fix the bug last. Build the defence first.
The Whack-a-Mole Trap
Consider a bug where a function returns null when it should return a username, and downstream code handles an empty string — not null — so the empty string passes all checks and processes silently as valid. You find the bug, fix the specific line, write a test for it, and move on.
Six weeks later, a different developer writes: const label = item.getLabel() ?? ''. Or an AI assistant autocompletes it. The same class of bug is back. Your fix addressed the symptom, not the cause.
This is the whack-a-mole trap: fixing individual bugs without addressing the structural vulnerability that permitted them. Each fix creates the impression of progress whilst leaving the underlying pattern intact and ready to reappear in the next sprint, the next feature, or the next developer's first week.
Each bug is evidence of a systemic vulnerability. Defend against the disease, not just the symptom.
The Six-Clause DBF Method
DBF started as a narrative description of a habit. It is now a versioned specification — Method Specification 1.0.1, published alongside a Detector Specification for tool maintainers and a Toolchain Specification for project tooling. The specification is the source of truth; where this article and the specification differ, the specification is correct. It sets out six clauses, in order. Resist the urge to jump straight to the last one.
Read it in full at defence-before-fix.github.io/SPEC.html[src].
Clause 1: Attribute the Defect to a Class
The question is not “what went wrong here” but “what kind of thing is this an instance of”. Name the pattern, and write one sentence stating the hazard it carries — the harm it does, which need not be a crash. Error hiding counts. Code nobody can safely reason about counts too.
Then search the codebase by at least two independent techniques — a text search for the token and a reading of the code paths that consume the value are independent; two spellings of the same text search are not. Keep searching until the last technique adds nothing new. Widen the class if the search finds instances the rule would miss; narrow it if the rule would flag code that does not carry the hazard. A rule that matches only the exact line from the bug report is an instance detector wearing a rule's clothes.
Clause 2: Build the Net
Express the class as a rule in a detector — a tool that reads code rather than running it. A test cannot do this job: a test proves that one input produces one wrong output, while a rule finds the pattern wherever it occurs, including in code nobody thought to test. For PHP codebases, PHPStan and Psalm both support custom rules. For TypeScript, ESLint custom rules handle this. Where no extensible detector exists for a language, a bespoke one — a project-owned program that reads code and reports matches — is a legitimate last resort. Only where even that is impractical is the class left undefended, and that gap must be recorded, not quietly treated as out of scope.
Clause 3: Prove the Net by Making the Rule Fire
A new rule must be proven to fire before it is trusted. It is never the goal to write a rule and be instantly green — a green run on the first attempt usually means the rule does not detect what it was built to detect, not that the codebase is clean. Run it against the instance that sent you looking, or against fixture code if the pattern is not currently present, and keep that red run as a commit of its own, before the fix lands. That commit is the only record that the rule actually caught real code, rather than only a fixture invented to flatter it.
Clause 4: Sweep the Codebase, Then Fix Every Instance
Run the rule across everywhere the pattern can occur — usually a whole language, never just the file or service that happened to report the bug — and record the total count before fixing anything. Corroborate that count with the same independent search from clause 1; a rule drawn too narrowly produces a small, clean-looking sweep, and nothing inside the rule itself can reveal that it is missing instances. Then fix every one. Under AI-assisted development, baselining the existing instances so the rule blocks only new ones is almost never the right answer — fixing a few hundred instances is cheap for an agent, and reaching for a baseline is usually a habit rather than a genuine constraint.
Clause 5: Enforce Permanently, and Block
The rule becomes a permanent part of the project's quality checks, and it fails rather than warns. A rule that reports a violation without failing the build does not conform — a warning is a suggestion, and suggestions do not survive contact with a deadline. This applies to everyone committing to the project, not just whoever remembers the rule exists.
Clause 6: Make the Failure Message Terse, and Point It at Real Documentation
The failure message itself stays short — it is read mid-task, by someone who wants to get on with something else. But it carries a stable identifier that resolves to documentation shipped with the project, stating what the rule is about, why it exists, and how to fix a violation correctly. “Pattern X detected” teaches nobody anything. A message that only prohibits, without showing the correct construction, blocks an AI assistant without redirecting it — and it will guess at the replacement.
None of this replaces ordinary test-driven development. The specific defect is still reproduced with a test and proven fixed, exactly as normal — DBF operates one level above that, on the class rather than the instance. A test pins one behaviour; a rule catches the whole pattern, wherever it occurs.
Three Error-Hiding Patterns
Three structural patterns account for a disproportionate share of silent failures — bugs that do not crash the system immediately but corrupt its state over time.
The Silent Default
Null coalescing — the ?? operator in PHP and TypeScript — is useful when a genuine default value makes sense. It becomes dangerous when used to replace missing data with an empty value: const username = user.getName() ?? ''.
The function returned null because the data is absent or an error occurred. The null coalescing replaces that signal with an empty string, which passes all downstream validation and processes as if everything is normal. The bug is invisible until something downstream breaks — often far from the source.
The DBF defence is a static analysis rule that bans ?? '' and ?? 0 patterns, forcing explicit null handling instead.
The Empty Catch
An empty catch block is an exception graveyard. The code inside the try block throws because something went wrong — a payment failed, an external service returned an error, a file was not found. The empty catch discards that information entirely. The system carries on in an unknown or corrupt state.
Even a catch block that only logs the error before continuing is nearly as dangerous — it records the failure but does not stop the corrupt processing chain. Errors must be handled explicitly or re-thrown for callers to handle.
Implicit Type Coercion
Languages that silently convert between types allow logically invalid code to pass without complaint. PHP's loose comparison (== instead of ===) is a classic source: "1admin" == 1 evaluates to true in PHP because the string is coerced to an integer. TypeScript without strict mode permits implicit any, removing type safety entirely from affected code paths.
The DBF defence is strict compiler and analyser configuration applied across the entire codebase — not left as file-by-file opt-in.
Language Implementations
Each major language has mature static analysis tooling. The DBF approach starts with enabling the strictest available standard configuration, then adds custom rules for patterns the standard configuration misses.
PHP: PHPStan at Maximum Level
PHPStan is the leading static analyser for PHP. Setting level: max enables every available check. The strict-rules extension adds constraints that catch the silent-default and empty-catch patterns that base PHPStan misses.
Every PHP file must also declare strict types at the top: declare(strict_types=1);. Without this, PHP's type system is advisory rather than enforced, and implicit coercion can occur even with PHPStan passing cleanly.
TypeScript: Strict Mode Plus Additional Flags
TypeScript's strict flag enables a cluster of checks including strictNullChecks and noImplicitAny. Several additional flags beyond strict catch patterns that strict mode alone misses.
noUncheckedIndexedAccess is particularly powerful: array index access returns T | undefined instead of T, forcing explicit handling of the case where the index does not exist — a common source of silent runtime errors that strict mode alone does not catch.
Building Custom Rules
Standard configurations catch common patterns. Custom rules encode organisation-specific knowledge — lessons from your particular bug history that no generic tool would know to check. This is the heart of DBF: your codebase accumulates a permanent record of every significant bug class it has encountered.
Creating a custom ESLint rule for TypeScript takes roughly 30–60 minutes for a developer familiar with the tooling. The rule runs on every file, on every commit, for the lifetime of the project.
The rule above bans ?? '' and ?? 0 and provides a clear error message explaining the reasoning. Future developers — and AI assistants — see the error and understand why the pattern is rejected, not just that it is.
A custom rule is institutional knowledge that cannot be lost, forgotten, or overridden by a new hire.
The Ratchet Effect
Codebases with an active DBF practice undergo a structural change over time. Each rule added is permanent — a ratchet tooth that moves the quality floor upward and keeps it there. Rules represent lessons encoded into the build process; they are never removed.
In year one, rules surface violations as legacy code is addressed. In year two, new code is written clean from the start — developers have internalised the constraints. By year three, the codebase has structural memory of every significant bug class it has encountered. Code review focuses on logic rather than pattern policing. New developers are constrained to safe patterns from their first commit, without reading incident reports from years past.
The cumulative effect is a codebase where entire categories of bugs become structurally impossible to introduce — not because people are more careful, but because the build refuses them.
DBF and AI Coding Tools
DBF has always mattered. It matters considerably more in the age of AI-assisted development.
AI coding tools generate code at a pace that overwhelms traditional human review. They reproduce patterns from their training data, which includes vast quantities of code written before modern static analysis standards existed. They will happily autocomplete const username = user.getName() ?? '' because that pattern appears in millions of training examples.
DBF rules act as a quality floor that AI-generated code must clear before it can be committed. The static analyser does not care whether a pattern was written by a human or an AI — the rule fails either way. This makes AI coding tools substantially safer to use at scale, without proportionally increasing the review burden on your team.
The specification is explicit about where authority sits when an AI assistant is doing the work. An agent decides how the defence is built — the class, the rule, the fix at each instance. It does not decide what the codebase is permitted to keep: adopting a baseline, suppressing an instance, or leaving a known instance unfixed are decisions for whoever owns the codebase, never the agent's to take on its own authority, however large the instance count turns out to be. An agent that hits one of those decisions finishes everything else within its own authority, reports the count and what fixing it would take, and leaves the rule unmerged rather than merged in a weakened form.
DBF turns your static analyser into a quality filter for AI-generated code.
Starting Your DBF Practice
DBF does not require a major tooling overhaul. Most codebases already run some form of linting or static analysis — the question is whether that tooling is at its strictest available configuration, and whether custom rules are being written when bugs are found.
Three changes have the highest immediate impact:
- Enable the strictest standard configuration for your language: PHPStan level max, TypeScript strict mode with additional flags, mypy strict. Address violations incrementally — a codebase under analysis is better than none.
- Establish a team norm: when a bug is fixed, a static analysis rule is written first. Make it part of the definition of done for every bug fix.
- Write your first custom rule for the most recent significant bug your team found. This builds the habit and demonstrates the pattern to the rest of the team.
The term was first published in February 2026, in the original article on ltscommerce.dev — the piece that holds the coinage date. The dedicated Defence Before Fix site is now the canonical reference: the versioned Method, Detector, and Toolchain Specifications, a shorter primer for newcomers, and a register of conforming tools for PHP and TypeScript. Where the two disagree, the specification governs.
defence-before-fix.github.io[src]
ltscommerce.dev/articles/defence-before-fix-static-analysis[src]
Ready to eliminate your technical debt?
Transform unmaintainable legacy code into a clean, modern codebase that your team can confidently build upon.