It’s easy to write code that works most of the time. Feed it the expected inputs and it produces the expected outputs. But give it something surprising — an empty value, an unexpected type, a number out of range — and it might fall over entirely.
Defensive programming is the practice of writing code that anticipates this. Instead of assuming everything will go right, you assume it might not, and you build accordingly.
There’s a useful distinction to make here.
Correct code produces the right output for every possible input. In theory that’s the goal, but in practice the range of possible inputs is often enormous and impossible to fully test.
Good code goes further. It’s robust enough to handle unexpected inputs without crashing, efficient enough to perform under real conditions, and clear enough that someone else can maintain it. Good code is correct and built to survive contact with the real world.
Correctness is necessary but not sufficient. You can write logically correct code that’s brittle, hard to follow, and impossible to extend. Good code is correct and built to survive contact with the real world.
Most bugs don’t come from bad logic — they come from assumptions. Common ones:
These assumptions feel reasonable when you write them. They’re how bugs get in. The moment you think “this can’t happen,” you’ve created the conditions for it to happen.
Murphy’s Law applies directly here: if code can be used incorrectly, it will be. Defensive programming means taking that seriously — thinking through what might go wrong at each stage and guarding against it before it does.
Defensive programming is often confused with error checking, testing, or debugging. It’s none of those things, though it works alongside all of them.
The distinction matters. Debugging is reactive — you’re responding to something that already broke. Defensive programming is proactive — you’re thinking ahead so there’s less to debug.
Defensive programming has a real cost. Extra checks mean extra code, and extra code means slightly slower execution. In a large system with hundreds of thousands of functions, that overhead can add up.
The counterargument is straightforward: catching a problem early costs far less than tracking it down after it’s escalated. Hours spent writing defensive code saves days spent debugging. Code that runs slightly slower but reliably is worth more than code that’s fast but occasionally breaks.
Most defensive techniques also have negligible performance impact in practice. The overhead argument is real in principle but rarely decisive.
Most mistakes are prevented before they happen by writing clear, well-structured code from the start. Meaningful names, consistent formatting, and logical structure reduce the surface area for bugs. If you start with a clear design — defined responsibilities, well-structured components, clean interfaces — you avoid a large class of problems that come from patching poorly organised code.
Think about each line as you write it. What could go wrong here? Have you handled every logical case? Slow, methodical programming produces fewer bugs than rushing through implementation and fixing them later.
A common trap: writing the main flow first and planning to add error handling “after.” That after rarely comes. Write defensive code as you go, not as a second pass.
Assume any input to your code could be wrong — from users, from other functions, from external libraries, from the operating environment. Even well-intentioned callers make mistakes.
# Don't assume this will always be a valid positive number
FUNCTION calculateDiscount(price):
IF price <= 0:
RETURN error
RETURN price * 0.9
Treat all inputs as suspicious until you’ve verified them. That includes inputs from your own code.
When you have to choose between concise and clear, choose clear. Complex one-liners are impressive until someone has to maintain them — including you, six months later.
# Hard to verify at a glance
result = (a + b) / (c - d) * e
# Easier to check each step
sum = a + b
difference = c - d
result = (sum / difference) * e
Splitting complex expressions into steps makes logic easier to follow and errors easier to spot.
Internal state should stay internal. If a variable or function is meant for use only within a module or class, enforce that — don’t rely on documentation or convention.
In object-oriented languages, make internal data private. In any language, keep variables in the tightest scope necessary. A variable that only belongs inside a loop shouldn’t exist outside it.
Compilers can detect a wide range of potentially dangerous code — uninitialised variables, type mismatches, unreachable code. Most developers either disable these warnings or ignore them. Don’t.
Enable all warnings and treat them seriously. If your code generates a warning, fix the code. Leaving warnings in place trains you to ignore them — and eventually one of them will be pointing at a real bug.
Compiler warnings are a form of static analysis — inspecting code before it runs. Dedicated static analysis tools go further, catching patterns that compilers miss. Make them part of your regular workflow.
Fixed-size data structures without bounds checking are a common source of bugs and security vulnerabilities. If you write past the end of a buffer, you corrupt whatever is stored next to it in memory — and in the worst case, a malicious user can exploit that to run arbitrary code.
Use data structures that handle sizing automatically, or use size-limited operations when working with fixed buffers. Never write to a buffer without knowing it has enough room.
If a function returns a value, it’s doing so for a reason. Check it. Ignoring return values — especially error codes — lets failures silently propagate through your program until they surface somewhere unrelated and much harder to debug.
result = saveFile(data)
IF result == error:
handleError()
This applies to your own functions and to library functions equally.
Every resource you acquire — memory, file handles, network connections, thread locks — should be explicitly released when you’re done with it. Don’t rely on the operating system to clean up when the program exits. You don’t know how long the program will run, and in some environments OS cleanup isn’t guaranteed.
Release resources as close to where you acquired them as possible, and make acquisition and release explicit.
Declare a variable and assign it a value in the same place. An uninitialised variable can hold whatever value happened to be in memory at that location — producing unpredictable behaviour that changes between runs.
# Bad — value is unknown until assignment later
count
...
count = 0
# Good — intent is clear from the start
count = 0
Even if the initial value is a placeholder, explicit initialisation makes the code’s intent clear and prevents hard-to-trace bugs.
Wherever you make an assumption about program state, make it explicit in code rather than leaving it as a mental note.
FUNCTION divide(a, b):
ASSERT b != 0 ← documents and enforces the assumption
RETURN a / b
This technique — sometimes called design by contract — turns hidden assumptions into visible checks. Preconditions verify that inputs are valid before a function runs. Postconditions verify that outputs are correct before returning. Invariants confirm that program state remains consistent at key points.
Constraints like these are most useful during development. Many languages let you strip assertion checks from production builds to avoid overhead, but while you’re building and debugging, they catch logic errors early and make your assumptions visible to anyone reading the code.
Defensive programming is a collection of habits, not a single technique. The thread connecting all of them is the same: don’t assume things will go right. Validate inputs, handle failures, keep state private, write clearly, and make your assumptions explicit in code rather than in your head.
Good defensive code also documents its own assumptions — through names, constraints, and assertions — making it easier for the next person (or future you) to understand what the code expects and why.
None of these techniques eliminate bugs entirely. But they catch problems earlier, make them easier to find, and prevent small mistakes from escalating into failures that are expensive to track down. A little extra time spent defensively at the start saves much more time later.
The next unit covers debugging — what to do when something goes wrong despite your best efforts.