Unreadable is not zero
The costliest type confusion in finance: an account that fails to read is unknown, and unknown is not empty. Three places my system nearly treated a timeout as a balance.
The bug class that scares me most in money software isn’t a crash. A crash is loud. It is the quiet coercion of I couldn’t read it into it’s zero — a type error wearing a default value, and it walks straight through most codebases because empty collections and failed reads produce the same shape of object.
Three places in my system had this trap. Each needed a different answer, which is the actual lesson.
Case one: the ledger fixer
A maintenance path corrects internal position records from a live broker read — the broker is the external truth, so on divergence, take the broker’s number. Now let the broker API time out and return nothing. Coerce that to “holds nothing” and the fixer diligently zeroes the ledger. The next rebalance, netting against a suddenly empty book, re-buys the entire portfolio.
The rule here: refuse outright. An unreadable account is not evidence about the account; it is the absence of evidence. The fixer exits loudly and writes nothing. Auditors have distinguished these for a century — no evidence of a balance and evidence of no balance are different findings, and only one of them supports a write-off.
Case two: the funding gate
Before placing a buy, the runner reads the live cash balance; if it is short, the order defers instead of bouncing. So what happens when the balance read itself fails? Fail closed — treat unreadable as insufficient — and every transient API hiccup freezes the whole book’s buying for a session. The gate exists to prevent a rare, recoverable annoyance; failing closed would create a common, systemic one.
The rule here is the opposite: stand down. The gate skips its check and lets the order proceed to the broker, whose own funds check is the real enforcement anyway. A guard rail should degrade to the pre-guard-rail world, not to a lockdown.
Case three: the top-up sizer
A restore job tops raw cash back up to a working buffer by redeeming from a parked fund. The gap it computes is only as good as the balance it read. So the sizer carries a ceiling: a computed gap larger than a set bound is refused and surfaced, not filled — because a huge apparent shortfall is more likely a bad read than a real need, and the failure mode of trusting it is liquidating the entire park on a phantom number.
The rule here: bound the blast radius. When a computed action’s size is a function of one fallible read, cap the action, not just the input.
The framework
Same type confusion, three different correct answers — refuse, stand down, bound — which is exactly why “handle errors” is not a policy. The policy is:
For every external read a system acts on, decide at design time what happens when the read fails — and price each option by its worst case, not its likelihood. Zeroing a ledger compounds; skipping one funding check doesn’t. Freezing a book costs every position; refusing one top-up costs a day of drag.
And the smell to grep for: any code path where a caught exception, a timeout, or a missing key flows into the same variable that elsewhere holds a genuine zero. None and 0 agree about almost everything, right up until they cost you the book.