DEV Community

Cover image for I Was Arrested for Guarding Too Much: A Trial About the Null Check That Never Fired
Izak T
Izak T

Posted on

I Was Arrested for Guarding Too Much: A Trial About the Null Check That Never Fired

The charge: three counts of over-guarding. The evidence: one ternary that had never run. I was not the hero of the trial that followed.

TL;DR A guard should test what can actually happen at that layer. If the view already guarantees a value is non-null, checking it again in the script adds no safety — it adds noise, hides the real contract, and quietly turns bugs into wrong output. The ruling: guard at the boundary, once. Let interior code trust it.


The Arrest

It happened over a schedule board. Two guards came for me — guards, yes, for a man who guards too much.

The charge: over-guarding. Three counts — which three, the paperwork didn't say. The evidence bag held only one exhibit — a Vue computed property, three lines long, the ternary bagged separately:

const pickTechName = computed(() =>
  pickTarget.value ? trayTechName(pickTarget.value) : ''
//  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ this part
)
Enter fullscreen mode Exit fullscreen mode

It names the technician shown in the click-to-place banner on our schedule board. It also, per the arresting officer, stands guard against a null that could never reach it — a doorman paid to open a door that never gets a visitor.

The charge sheet was signed by the prosecutor — a colleague who had read the diff:

"Why do we check for null here when the view already guarantees it?"

I had no answer ready. The prosecution cannot compel a defendant to testify — so taking the stand was my counsel's last card: "The defense calls the defendant." What follows is the transcript of my cross-examination.


Exhibit A: The scene of the alleged crime

The computed has exactly one reader in the entire codebase:

<div v-if="pickTarget">
  <span class="font-semibold">Placing "{{ pickTarget.title }}"</span>
  <span v-if="pickTarget.techId">
    — click an open hour on {{ pickTechName }}'s row
  </span>
</div>
Enter fullscreen mode Exit fullscreen mode

Note the two v-ifs already standing at the door. The banner exists only while pickTarget does; the hint, only when that job actually has a technician — an unassigned job is a real state here, not an exception. The computed is only ever read inside that block — after both checks have already passed.


Cross-Examination, Round One

Prosecutor: Why is that ternary there?

Me: Because pickTarget starts as null. trayTechName(null) would throw — it reads job.techId off the argument. The guard prevents a crash.

Prosecutor: When is the computed evaluated?

Me: When it's read.

Prosecutor: And it's read where?

Me: Inside the v-if="pickTarget" block.

Prosecutor: So at the moment of evaluation, can pickTarget be null?

The court reporter stopped typing.

My counsel rose: "Objection — the question assumes facts that haven't been established."

Overruled. The fact was about to be established anyway. computed in Vue is lazy: the getter does not run at setup. It runs on first read — and the only read is inside a subtree that requires pickTarget to exist. By the time my guard could fire, the condition it tests is already guaranteed false.

My counsel leaned over and whispered: "They have a point. Sit tight — let me see what else we have."


Cross-Examination, Round Two

Prosecutor: Very well. It's redundant today. But isn't it cheap insurance for the future? Someone might read it unguarded later.

Me: It's one comparison. It costs nothing.

Prosecutor: I am not asking what it costs. I am asking what it communicates.

My counsel tried again: "Objection — counsel is testifying."

Overruled. The prosecutor's point stood.

That is the question I had been avoiding. A guard is not a neutral act. It is a claim. It says: "this value can be null at this point." A reader arriving later now has to hunt for the scenario where that's true — and there is none. I had invented a hypothetical future reader to justify a check with no failure mode today.

"Cheap" was never the argument. "Honest" was.


Recess: The Three Kinds of Guards

During the recess, my counsel and I put together a table to explain our defense. Every guard is one of three things:

Kind Ask yourself Where it goes Example
Boundary guard "Can this outside data be wrong?" Only where data comes in from outside Reading localStorage, an API, a form
Invariant guard "Can my guess be wrong?" Inside, where it must shout if wrong assert(items.length > 0) before indexing
Redundant guard "Is it already known?" Nowhere — someone already answered Re-checking what the view already guarantees

Same question at every layer — and only three answers: yes, never (and say it loudly), or already answered. Only the first two need a check.

A guard should test what can actually happen at that layer — not re-test what a neighbor already guarantees.

The prosecutor rose to answer our table. "This distinction is not just opinion. P2053R1, a C++ committee proposal by Rostislav Khlebnikov and John Lakos, draws the same line: a check is 'defensive' when its assumption is expected to always hold. Human input and remote clients violate assumptions — those need validation at the boundary, not in trusted interior code."

(The full proposal, plus Lakos's CppCon talk summary in plain HTML, cover the same ground.)

My counsel leaned toward me. "But what about spacecraft? A bit flipped by radiation can turn an impossible state into a real one — that's why NASA votes outputs across triple-redundant avionics."

The prosecutor nodded. "Warranted, there — hardware itself can corrupt state. But this is a Vue component, not flight-control software. The template reads this computed only after pickTarget passes its v-if. This ternary doesn't guard against physics; it repeats a software guarantee."


The Prosecution's Case: Three Counts of Over-Guarding

When court resumed, the paperwork finally became specific. The prosecutor read out the three counts — and none of them concern performance.

Count one: it lies about the contract. The guard advertises a possibility that doesn't exist, taxing every future reader with a search for a phantom — after validation, later inconsistency is an internal error and should hard-fail (Michael Belivanakis).

Count two: it masks real bugs. If the invariant ever does break, the guard substitutes '' and renders a slightly wrong UI — instead of throwing a stack trace that points straight at the culprit. John Lakos's CppCon talk summary puts it as near-consensus: "Most would agree that a well-implemented library should do something other than silently continue when a pre-condition violation is detected." The pithier form comes from a heavily upvoted Software Engineering Stack Exchange answer: "isn't the null check, it's the silent fail" — programmer errors are best dealt with by failing immediately and loudly.

Count three: it breeds. Once neither caller nor callee can say who owns the check, every layer starts checking "just in case" — and the codebase fills with guards that guard nothing. This one has the best citation, and it comes from the man who invented the alternative.


The Evidence: Precedent from 1988

My counsel rose once more — keeping his promise from Round One to see what else we had. "I went looking for precedent that might excuse a 'just in case' guard, Your Honor. What I found is older than my client — and it is not on our side. The defense offers no opinions. Only precedent."

Bertrand Meyer's *Design by Contract* — the contract theory behind Eiffel — rests on a rule he calls non-redundancy. In his own chapter on the subject, Meyer states it as an absolute rule:

"Either you have the condition in the require, or you have it in an if instruction in the body of the routine, but never in both."

And he names the cost: redundancy "accumulated over a large system" buries "islands of useful processing in oceans of error-checking code."

The official Eiffel documentation states it operationally — "a routine body should never test for the precondition, since it is the client's responsibility" — and records the counterintuitive payoff: "one can get more reliable software by having fewer explicit checks in the software text."

And in "Applying Design by Contract" (IEEE Computer), Meyer dismantles the exact argument I made on the stand — the "just in case" guard:

"Adding possibly redundant code 'just in case' only contributes to the software's complexity... The result of such blind checking is simply to introduce more software, hence more sources of things that could go wrong at execution time, hence the need for more checks, and so on ad infinitum."

There it is: my "cheap insurance" answer, refuted in 1992 by the man who defined the contract. The court accepts the precedent. I had been arguing against case law I didn't know existed.


The Defense's Best Argument: "But what if a user edits localStorage?"

My counsel played his last card — our strongest argument. "What if a user opens devtools and hand-edits the stored JSON? Then the data genuinely can be wrong, and the guard is justified."

It deserved a real answer rather than a shrug.

The threat model is wrong. A dispatcher at a small business is not going to open devtools and hand-edit a JSON blob. If your safety depends on that being unlikely, you have larger problems than a null check — Instil's write-up is blunt that below the public boundary these checks are overhead meant for tests and review, not runtime guards.

And this is the part I got wrong: the real reasons storage needs care have nothing to do with attackers. They are mundane — an earlier version of the app wrote a different shape, the storage filled and the write silently died, two browser tabs raced to write the same key, a user cleared their browsing data. None of that justifies guards sprinkled through the app. All of it justifies one guard, at the read boundary, where bytes become objects.

So the objection doesn't defeat the rule — it confirms it. Reading localStorage is a boundary: data coming in from outside the app's control. And boundaries are guarded once, at the edge.


The Court's Finding: Where the Guard Belongs in Vue

Before the jury retired, the judge settled the one question the case actually turned on: in a Vue component, who owns the guarantee?

There is a twist specific to Vue worth entering into the record: the template is a boundary.

That is not just convention — it is design. Vue's docs describe the single-file component as "colocating the view, logic and styling of a component" — one component, one shared contract. Which is why they live in one file: split them, and neither side sees the whole guarantee — how duplicate checks get born.

TheCodeForge's guide to the classic Vue template crash explains why: "Templates don't wait. That first render is where nearly every Cannot read properties of undefined is born." Their fix is the same instinct: "fix at the data boundary in setup(), not with scattered guards in every template."

And the idiomatic Vue form is a container v-if — the pattern you already saw in Exhibit A. SoundsCode's lesson says it plainly: the condition narrows the type for everything in that subtree.

One condition. One owner. Compare that to sprinkling pickTarget?.title, pickTarget?.techId, and a ternary inside the computed — three checks, three places to drift, and no single line that states what is actually guaranteed.

One honest footnote, because a sharp reader will spot it: pickTarget is read a second time outside that subtree — :selected-job-id="pickTarget?.id || null", handed to the unscheduled tray, which is always mounted. That optional chain is not the same species of check, and it should stay. There, null is a legitimate value — the tray renders whether or not anything is picked, and the child's prop is typed to receive String | null. That is a boundary handoff, not a redundant guard.

The rule was never "never use ?.." It is "use it where the value can actually be absent" — and the taxonomy above discriminating between two reads of the same ref is exactly the point.


The Verdict

Guard at the boundary, once. Let interior code trust it. Where you can, structure the code so the invalid state is impossible; where you cannot, fail loudly rather than substituting a value.

The template's v-if="pickTarget" is the owner. The computed should not re-litigate it. The corrected exhibit:

const pickTechName = computed(() => trayTechName(pickTarget.value))
Enter fullscreen mode Exit fullscreen mode

If someone later reads it unguarded, it throws — loudly, at the exact line that is wrong. That is not a bug. That is the contract doing its job.

A fair objection: a throw during render can blank the page in production. It is still the better trade — a silent '' hides the defect forever; a throw surfaces it. "Fail loudly" was never "crash the customer." It means fail where you can see it: the console in development, and an error boundary in production — Vue's onErrorCaptured or app.config.errorHandler, which reports the bug and renders a real error state instead of a card full of blanks.

The rule is not "never guard." It is "never silently substitute."

The jury was out for twenty minutes. When they filed back in, the foreman read the verdict to the judge.

Guilty on all three counts. The guard is released from duty — it never worked a day in its life.


Closing Remarks

The lesson was never about null checks. It is that my first two answers were shaped like reasoning while being motivated reasoning — an instinct, then a hunt for justifications instead of facts. The prosecutor was right the entire time; the record only changed when I read the sources and noticed the check I had been defending had never once executed.

The sentence was lighter than I deserved: one deleted ternary, and a paragraph in the project's decision log.

A guard that guards nothing is cheap.

Defending it in court is not.

Top comments (0)