The most dangerous output a data pipeline can produce is not an error. It is a well-formed zero.

Over four days in January, a risk dashboard I was retained to audit reported a client's collateral position at exactly 0.00. No alert fired. No page went out. The panel stayed green, because green was the color assigned to "no exceptions." The pipeline had not crashed. It had succeeded — at producing nothing, and the serializer had formatted nothing as a number with two decimal places and a currency symbol.
In a different repository that same week, a Solidity mapping returned 0 for a key that had never been written. The contract behaved exactly as the language specifies. No exception, no log, no event. A transfer of zero tokens to the zero address, executed by a keeper bot that had been told the balance was real.
Neither system was broken. Both were correct. Both were lying.
Empty is not the absence of a state. It is a state — and in systems that are forbidden from lying, it is the most dangerous state there is. That distinction, between "I have no answer" and "the answer is zero," is where the next generation of smart contract exploits will live. Not in reentrancy. Not in integer overflow. In the quiet, spec-compliant, perfectly-typed empty result that nobody thought to audit.
The Pipeline That Never Says No
To understand why this is a structural problem rather than an engineering one, you have to look at how on-chain data actually reaches a decision. There are four layers, and every one of them has a documented, sanctioned way to return nothing.
Layer one is the chain itself. eth_getLogs against a node that has pruned the relevant state, or against a block range wider than the provider's cap, will frequently return an empty array rather than an error. An empty array is a valid JSON-RPC response. The client cannot distinguish "there were no Transfer events" from "I did not look at the blocks you asked about." Both serialize to [].

Layer two is the indexer. A subgraph that has not yet synced to the requested block height returns an empty result set for entities in that range. The Graph's query semantics do not carry a "not yet indexed" flag into the response body. Downstream, an unindexed block range and a genuinely empty transfer history are the same three characters.
Layer three is the oracle. Chainlink's latestRoundData() returns a five-tuple. Two of those fields — updatedAt and answeredInRound — exist precisely so that a consumer can tell a fresh price from a stale one. In practice, an entire generation of integrations reads the tuple, checks price > 0, and discards the rest.
Layer four is the contract. Solidity's default values are deterministic and total: every uninitialized uint256 is 0, every uninitialized address is address(0), every uninitialized bool is false, every field of a freshly allocated struct is zeroed. There is no null. There is no undefined. There is no exception. The type system guarantees that you will always get an answer, and that the answer will be indistinguishable from a real one.
Four layers. Four places where absence and zero collapse into the same byte pattern. The chain is not indifferent to your intent; it is indifferent to your vocabulary. It has no word for "I don't know," so it says zero, and zero is a number you can trade against.
This is not a hypothetical taxonomy. It is the shape of the failure I keep finding. Trust nothing. Verify everything — including, and especially, the fields you did not read.
The Stale Round Problem, Restated
The canonical version of this bug has been written about so many times that auditors have stopped seeing it. Let me restate it at the level where it actually bites.