Founder Notes
Who Was Asking
A page on my personal site was empty for weeks, and I spent the first hour of debugging it looking in exactly the wrong place.
The symptom: an index page that lists posts showed nothing at all. The individual post pages worked. You could reach any one of them directly and read the whole thing. Only the list was blank.
That asymmetry should have told me the answer immediately. Instead it sent me hunting through the listing query, which was — of course — correct. It had always been correct.
Two readers, one query
Here is what was actually happening.
The individual pages are rendered when the site is built. The build runs with credentials, because building is a trusted operation and somebody set it up that way long before any of this mattered. The index page re-fetches at request time, in production, as nobody in particular.
Those are two different readers. The same query, sent by each, gets two different answers — because the document type the blog is built on was not readable without credentials, and nobody had ever noticed. The pages people actually linked to had been baked at build time, by the one reader that could see them.
So the site was, in a precise sense, displaying content its own live queries could not retrieve. The prerendered pages were a photograph taken by someone with access, hanging in a room that nobody with access ever enters again.
The empty array
The second half of the failure is the part I actually want to talk about.
The fetch helper had a fallback. If the query came back with nothing usable, it returned an empty list. Sensible-looking defensive code. Every codebase has some.
What that fallback does is collapse two entirely different situations into one value:
- there are no posts
- you are not allowed to see the posts
From the outside these are identical. Nothing logged, nothing thrown. A page that renders correctly and says, in effect, there is nothing here — which is a confident factual claim about the world, made by a process with no standing to make it.
An empty result is not a fact about the data. It is a fact about the reader. And almost nothing in the stack shows you the reader.
The same lesson, wearing different clothes
A day later I hit the identical shape somewhere else and did not recognise it.
Every call to an image-generation service was failing as out of credits. I checked the balance in the console: plenty there. Checked again: still plenty. Concluded, eventually, that several providers had all been exhausted at once — the kind of conclusion that should have embarrassed me at the moment I reached it.
The key in the configuration belonged to a different account than the one whose balance I kept looking at. It was genuinely, accurately out of credits. The error was true. It was true about an account I had not intended to be using, and like every error message of its kind, it described the world it found itself in rather than the one I thought I was in.
I had spent the whole time carefully verifying the answer to a question nobody was asking.
What a credential actually is
Most of us carry a mental model where the credential is a gate. You present it, the gate opens, and then you are inside doing the real work. Under that model the credential has two outcomes, in and rejected, and rejection is loud.
That model is wrong in a specific way. The credential is not a gate in front of the query. It is part of the query. It selects which account, which dataset, which subset of rows — which world. Change it and you have not changed your permission to ask. You have changed what you asked.
Which means the failure mode is not rejection. It is a valid, well-formed, truthful answer to a slightly different question, delivered with no indication that the question moved.
What I do differently now
Three things, none of them clever.
When a listing is empty and the detail pages work, I stop looking at the listing. That asymmetry is not a bug in either of them. It is evidence that two readers exist and only one of them can see.
An unfiltered query that comes back with zero now says so out loud, and names the credential as the first thing to check. Not an error — zero might be legitimate. A signal that the two cases have not been told apart.
And before believing any number a service reports about itself, I confirm which account it is reporting on. "Out of credits" and "out of credits, on the account you meant" are different statements, and only one of them was ever on the screen.
The general form
Every query carries an identity, and that identity is almost always invisible — in the code, in the response, and in the error.
So when a system tells you there is nothing there, the honest reading is not there is nothing there. It is there is nothing there, for whoever this turned out to be. Most of the time those are the same sentence. The entire problem is that you cannot tell from the outside which time it is.