Engineering Journal
Schema Editor
Schema Editor

The bug class where optional chaining hides a dead code path

2026-07-23

TLDR: Optional chaining turns a missing-key lookup into undefined instead of a crash. That is great for reads you expect to sometimes miss, and quietly catastrophic for lookups that must hit for the code to do its job. When the key is wrong, the branch that depended on it goes dead silently.

The bug class

a?.b?.c says "if any link is missing, give me undefined and move on." That is the right call when a missing value is a normal case you handle downstream.

It is the wrong call when the lookup is load-bearing. If your logic only makes sense when the lookup hits, a silent undefined does not skip a nicety, it disables the whole point of the code. And because nothing throws, nothing tells you.

The tell for this class: an optional chain feeding a filter or a conditional, where an empty result and a "nothing is wrong" result are the same value.

Why the language produces it

Optional chaining was added to kill defensive if (a && a.b && a.b.c) ladders. It succeeded. But it collapsed two very different situations into one syntax:

The language cannot tell these apart. Both read x?.y. So a lookup that should be an invariant gets written with the same forgiving operator as a lookup that is genuinely optional, and the invariant violation becomes a silent undefined instead of a loud error.

The concrete instance

A rule checked whether two output pins were shorted on one net:

const outputs = net.pins
  .map(pin => SPECS[pin.component].pins[pin.id]?.role)
  .filter(role => role === 'output');

if (outputs.length >= 2) report(${net.id}: bus contention);

SPECS was keyed by the names IN1/OUT. The pins carried the ids in1/out. So SPECS[c].pins['out'] was undefined, the ?.role made it undefined instead of throwing, outputs was always empty, and the rule never fired. Every rule in the pack had the same shape and the same silent death.

The ?. was load-bearing and misapplied. It was there to guard against a component with no spec, a real optional case. But it also swallowed the not-optional case of a key that should have matched and did not.

The fix

Two parts. Re-key the data so the lookups hit:

const SPECS = {
  andGate: { pins: { in1: { role: 'input' }, out: { role: 'output' } } },
};

Then, separate the genuinely-optional guard from the must-hit lookup, so a real mismatch is loud during development:

const spec = SPECS[pin.component];
if (!spec) continue;               // legitimately optional: no spec for this part
const meta = spec.pins[pin.id];
if (!meta && DEV) {
  console.warn(unresolved pin ${pin.component}.${pin.id});
}
const role = meta?.role;

Now a missing component is handled quietly, and a missing pin, the load-bearing case, prints a warning during tests. The mismatch stops being invisible.

How to prevent the class

Ask one question at every optional chain: is undefined a value I handle, or a value that means I have a bug? If it means a bug, do not swallow it. Throw, warn, or assert.

For lookups that join two data sources, log every miss during a test run. A join that silently drops half its keys will flood that log, and the flood is the bug announcing itself.

And write the test that a known-good key resolves and a known-bad input still produces a finding. Optional chaining hides dead branches from your eyes; only an assertion on behavior drags them back into the light.

The generalizable lesson

?. answers "what if this is missing" with "keep going." That is a feature for optional data and a landmine for invariants. Before you write it, decide which one you have. If the code only works when the lookup hits, a silent miss is not graceful degradation, it is a dead branch wearing a green check.

Read this post in the full Engineering Journal →