Four things I have been told by my own systems in the last three months:
- 0 clicks on outbound links, on a page I could watch people click through.
- Clean from a typechecker, on a codebase with two type errors in it.
- Position 13 for a page that has never once ranked 13th.
- 200 OK from an endpoint that did nothing at all.
None of these was a bug in the ordinary sense. Nothing crashed, nothing threw, nothing appeared in a log with the word ERROR next to it. Each was a real value, produced by a real system, working exactly as written.
And every one of them was a statement about the world that the system had no business making, because in each case the thing that was supposed to do the measuring had not run.
The overloaded value
Pick any layer of your stack and there is a single value doing two incompatible jobs.
A database query returns no rows. Does that mean the table has no matching rows, or that the query never reached the table? Both are null.
A process exits 0. Does that mean it checked everything and found nothing wrong, or that it checked nothing? Both are zero.
A counter reads 0. Is that "we measured, and it was none", or "nothing was ever counted"? Both are 0.
A command produces no output. Silence is what success looks like. It is also what a program that did nothing looks like.
This is not a language design mistake anyone can point at and fix. It is a consequence of representing absence at all: the moment you have a value meaning "nothing here", it will be produced both by nothing being here and by the lookup not happening. Every layer inherits it. Every layer passes it upward, where it stops looking like an absence and starts looking like data.
The asymmetry that keeps it hidden
Here is what makes this specifically dangerous rather than merely annoying.
In all four of my examples, the incorrect reading is the reassuring one.
Zero errors. All checks passed. No such user. Nothing to report.
A false alarm gets investigated within the hour, because it is uncomfortable and somebody wants it to go away. A false all-clear gets investigated never, because it is exactly what you hoped to see and there is nothing to chase.
So the failure mode is not just that the two states are indistinguishable. It is that the wrong one is load-bearing for your peace of mind, and the entire purpose of the signal is to let you stop looking.
That is why these survive for months rather than hours. A green check is not a piece of information you evaluate. It is permission to move on.
Four layers, one shape
The instrument. A dashboard showed zero outbound clicks. The tracking worked correctly — for one specific kind of link. Every other link on the site was a plain anchor tag the counter had never been taught about. So "0" was accurate about what it measured and silent about the fact that it was measuring almost nothing. The number was not wrong. Its scope was, and a number does not carry its scope. (Zero Is Not a Measurement)
The compiler. I ran a typechecker four times before shipping, and it exited clean every time. The project used a config that points at other configs and contains no source files of its own — a normal, widely-generated layout. Given that config, the tool typechecks an empty set, finds nothing wrong with it, and exits zero. It was not being lenient. It was being thorough about nothing. (It Passed Because It Never Looked)
The statistic. A page reported an average search position of 13. It had never ranked 13th. It ranked around 6 for one group of queries and around 20 for another, and the mean of a bimodal distribution is a number describing a state that does not exist. Unlike the others this one is not even a failure — the average is computed correctly, from complete data, by a system with no faults. It still describes nothing real. (One Page Ranked #6 and #20 at the Same Time)
The database. A password reset endpoint looked users up through an API layer that could not reach the table it needed. The call failed every time it ran. But only the result was captured and the error was discarded, so the failure arrived as null, and null was read as "no such user" — which routed into a branch specified to return success while doing nothing, because revealing whether an account exists is a security leak. (A Discarded Error Becomes an Answer)
Four different layers. One shape: an operation that did not happen, represented identically to an operation that happened and found nothing.
The question that separates them
There is one question that distinguishes a real signal from a decorative one, and it takes about a minute to ask:
If this were broken, what would I see?
If the honest answer is "exactly what I am seeing now", you do not have a signal. You have a value that happens to be reassuring.
Run it against the four:
- If the click tracker were counting nothing, the dashboard would read 0. It read 0.
- If the typechecker were examining no files, it would exit clean. It exited clean.
- If the average were hiding a split, it would return a plausible middle number. It returned 13.
- If the user lookup were failing completely, the endpoint would return 200 and send nothing. It returned 200 and sent nothing.
Four for four. In each case, "healthy" and "not running" produce byte-identical output, and I had been reading that output as evidence for months.
The follow-up question is the actionable one: what would have to change for these to be distinguishable? That is usually a small piece of work. Print how many files were checked. Report the count of rows the query considered, not just the result. Show a distribution alongside the mean. Return a different status code when the lookup itself failed.
None of that is clever. It is just refusing to let one value carry two meanings.
Unknown is a state you have to store
The fix that generalises is to stop modelling two states where there are three.
Not found and not found. Found, not found, and could not determine.
That third state has to survive as far as the decision. It is not enough to log it — logging is what you do with something you have decided not to act on. It has to reach the branch, and the branch has to treat it differently, because "I could not check whether this person has an account" and "this person has no account" call for opposite behaviour. One is a 500 that pages you. The other is a normal response.
In practice that means a handful of unglamorous habits:
- Bind your errors even when you do not think you need them. Discarding an error does not make it not happen. It converts it into whatever your success path does with a missing value, which is usually to proceed confidently.
- Make failure reachable, then check that it is. Break the check on purpose and watch it go red. If it stays green, you have not learned that your code is clean; you have learned that your check does not run.
- Require the failure to be specific. A nonzero exit says something failed. It does not say this check found this fault. Assert on the content, not just the status, or an empty run and a passing run remain interchangeable.
- Distinguish "no data" from "data says zero" in anything you display. They are different claims and a dashboard that renders them identically is lying quietly, every day, to whoever is looking at it.
Why this is getting harder
The volume of code arriving in my repositories that I did not type by hand has gone up sharply, and it is very good code by every measure my tooling can apply. It compiles. It is typed correctly. It follows the conventions of the file it sits in.
What it is not reliably doing is knowing things. It produces the plausible value confidently, and the plausible value is frequently right, which is exactly what makes the exceptions invisible. Generated code and swallowed errors have the same signature: something that looks like knowledge, occupying the place where knowledge should be, with no marker distinguishing it from the real thing.
Which means the burden shifts. It is no longer enough to check that the code is well-formed — every tool I have already does that, extremely well. The open question is whether the things it asserts are true, and almost nothing in a standard pipeline is built to ask.
The short version
Software very rarely fabricates information out of nothing. What it does constantly, at every layer, is take I could not find out and quietly round it to the nearest confident-looking answer — usually zero, usually null, usually success.
Then it hands you that answer with exactly the same face it uses for a real one.
Top comments (0)