The most expensive two characters in my codebase were `?? []`. What I found when I chased one bug class across a dozen screens, why a source scan beat code review at catching it, and why it matters most in anything that shows you money.
This month, in FieldDay, I fixed the same bug a dozen times before I admitted it was one bug. A parent with a family was told to "add your family". A person who had attended sessions was told their record "starts here". A calendar that failed to load said the kids had a free evening. None of these screens was broken. Each one was confidently wrong, and each one was wrong for the same reason.
Two characters.
The pattern looks like this:
const items = query.data ?? [];It's reasonable on its face. While the data is loading, or if there is none, you render an empty list. The trouble is that a request that failed also has no data. So the code can't tell "nothing is there" apart from "I couldn't find out", and it shows the same empty state for both. Your logbook is empty. You have no pickups tonight. Nobody is on your list.
Each of those is a claim. The screen isn't saying "I don't know". It's asserting a fact about a real person's life, and the fact came from a network blip.
The server version is the same bug with a catch. One function that built a household
view caught every error and returned hasFamily: false. A database hiccup turned
into "you don't have a family yet", and the UI dutifully invited a parent to create one.
Why fixing them one at a time didn't work.
The fixes were easy: render an error state with a retry, and don't draw a figure until the read has finished. I did that site by site, and I kept finding another one.
The reason is that every instance looks fine in isolation. The line is short, it's
idiomatic, a reviewer reads it as defensive programming, and the type system is happy
because [] is a perfectly good array. The bug has no stack trace and it fails no test,
because the tests only ever exercise the happy path and the empty path. It shows up as a
user quietly believing something false.
When I'd fixed it about ten times, I stopped fixing and wrote the rule down.
The rule, and a test that enforces it.
A failed read must never render as the empty state.
Then I turned it into a test, and the test is deliberately crude. It's a source scan.
It walks the files for a set of screens, finds any line that falls back from a query's
data to [] or 0, and requires that the same file also handles the error: an isError
check, an error status, or a helper that cannot return an empty list for a failed read.
I can hear the objection, because I'd have made it myself: grepping source is a hack. But the mistake here is a few characters long, invisible to the type system, and looks reasonable every time it appears. A scan is the right-sized tool. The lint rules I'd normally reach for need to understand the data flow, and a regex only needs to understand the typo.
Two details make it livable rather than annoying:
- An allowlist with reasons. Some fallbacks are safe. A component that renders nothing when its list is empty makes no claim about the person, so failure and emptiness can look the same there. Each exemption has to say why, in a sentence. The allowlist is short on purpose, and every entry reads like a tiny design review.
- The reason lives in the test file. The header explains the bug and why a scan is the tool, so the next person doesn't need the history I had.
- It closes the class, not the instance. A new screen can't reintroduce the bug without either handling the error or writing down why it doesn't need to.
What "handling it" looks like.
The fix is less about an error banner than about deciding what each screen is allowed to claim. I settled on three rules.
- Don't draw a figure before the read has settled. A spinner or a skeleton is
honest. A
0is not. - Say you couldn't read it, and offer a retry. "Couldn't read your household. Try again." is a complete sentence and a complete state.
- Partial is a third state. A feed that loaded half its cards should say that some of it didn't load, rather than ending with "Nothing more will load", which is a claim about the world.
There's a cousin of this bug that I fixed in the same stretch: an empty distance box on a
workout log showed a muted 0 at display size. A zero reads as a measurement that
somebody took. The box now holds a dash, which reads as "not given". It's the same idea at
the level of a single field: unknown needs its own representation, and it must not be
spelled as a number.
Why this is a money problem.
I've spent most of my career near ledgers, where the rule is that you may be late, but you
must never be wrong. A wallet is the sharpest version of this bug I can think of. If a
balance request times out and the screen shows 0 SOL, the user doesn't think "the RPC
node is slow". They think their funds are gone. Same for an activity feed that shows
"No transactions yet" because indexing lagged, or a portfolio total that quietly leaves out
the token whose price feed failed.
It's the principle I wrote about when turning a Solana transaction into an activity-feed
line: fall back to the facts,
and make the unknown look like an unknown. And it's the same rule I use in
/ask, where a weak retrieval says "I don't know" and does not
guess. A system people trust with something that matters has to be able to say "I couldn't
tell you" in a way that doesn't look like an answer.
What I took from it.
- Bugs that recur are usually one bug. After the third instance, stop fixing and find the class.
- If the mistake is invisible to your types and your reviewers, make a test that reads the source. Crude and exact beats clever and silent.
- Every empty state is a claim. Decide, screen by screen, whether you can stand behind it.
The satisfying part is that none of this is hard. It's two characters, a decision about what you're allowed to say, and a test to remember the decision. I'd have saved myself a dozen fixes by writing the test first.
Owen
Thanks for reading. Cheers.
If you spot something wrong in this post, or you’d have done it differently, tell me. I’d rather be corrected than confidently wrong. owenlrichards2000@gmail.com
— Owen