Avoid just-in-case programming
“Just-in-case programming” refers to code that isn’t sure about itself - adding unnecessary defensive measures driven by fear and uncertainty rather than actual requirements.
Examples of just-in-case code
Section titled “Examples of just-in-case code”Clearing data, just in case it is pre-filled
Section titled “Clearing data, just in case it is pre-filled”void fillData(vector<T>& vec){ // "Just in case" the vector isn't empty vec.clear(); // hidden destructive behavior (not visible at call-site)
/* ... */ // fill vec with new elements}
// Call sitevector<T> myVec;fillData(myVec);
// -- Simpler and safer alternative --
vector<T> someVec = makeMyData();makeMyData()approach removes ambiguity surrounding on the call site:- Will
fillData()overwrite or modify the input data? - Does
fillData()require an empty container as input?
- Will
Skip processing, just in case the data is empty
Section titled “Skip processing, just in case the data is empty”void processResults(const std::vector<Result>& results) { if (results.empty()) { // "Just in case" it is empty return; // Perhaps added as bugfix (e.g. call results.back() caused UB) }
/* process results */}The function is doing making a silent control flow decision instead of just processing input data. If you intend to add this kind of check, ask first:
- In which situation will
resultsbe empty? - Is that a valid situation, or a bug that we should detect earlier?
- Why does the caller attempt to process results, if there are none?1
This precondition check may signal a control flow problem. A call to processResults only needs to exist in a situation where there are actual results available to process.
Recommended approach:
- Understand where the results are coming from. On this boundary, immediately evaluate
- Is emptiness an expected, valid, outcome?
- Or is emptiness an invalid state, an error?
- Enforce the precondition
- Capture the non-empty state as early as possible and express it using a strong type that enforces the invariant
.size() > 1
- Capture the non-empty state as early as possible and express it using a strong type that enforces the invariant
if (auto results = fetchResults(id); !results.empty()) { processResults(NonEmpty(results));} /* if emptiness is an invalid state in your domain, then: */ else { /* throw an error */}
// Functions that require non-empty results do not need to duplicate the precondition check inside their body:void processResults(const NonEmpty<vector<Result>>& results) { /*...*/ }void storeResults(const NonEmpty<vector<Result>>& results) { /*...*/ }Problems with just-in-case programming
Section titled “Problems with just-in-case programming”- Obscures actual requirements - What are the real preconditions?
- Adds complexity without clear benefit
- Makes debugging harder - masks the real source of problems
- Performance overhead from unnecessary, often repetitive, checks
- Indicates lack of understanding of the code’s contract
The antithesis of intentional design
Section titled “The antithesis of intentional design”Just-in-case programming is the opposite of Make intentional design decisions. It’s driven by:
- Uncertainty
- Lack of trust in other code
- Cargo cult programming (copying patterns without understanding)
Better approaches
Section titled “Better approaches”- Understand your contracts - What are the actual preconditions and postconditions?
- Use assertions for debugging assumptions rather than defensive code
- Document or enforce preconditions explicitly
- Write tests that capture actual requirements
- Trust your interfaces - if they’re unreliable, fix them directly
Related
Section titled “Related”- Make intentional design decisions - The positive counterpart
- Write code that reads like the problem that it solves - Clear code reduces uncertainty
Footnotes
Section titled “Footnotes”-
It may be an intentional design philosophy to process data in a continuous stream as much as possible without branching (i.e. checking for
.empty()). In these data processing pipelines empty data and null objects are valid objects and typically processed via no-op behavior. ↩