Skip to content

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.

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 site
vector<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?

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 results be 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
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) { /*...*/ }
  • 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

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)
  • 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
  1. 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.