---
title: "Two nulls are not zero"
description: "A monitoring dashboard has three ways of not knowing something, and every one of them has been drawn as a confident zero at least once. Here is why that is the most expensive bug a status screen can have."
url: https://staging.harpywatch.com/blog/two-nulls-are-not-zero
kind: article
author: "The HarpyWatch team"
published: 2026-09-02T07:08:16.069924+00:00
tags: ["Design", "Reliability"]
publisher: HarpyWatch
---

# Two nulls are not zero

There is a category of bug that no test catches, no alert fires for, and no
customer reports — because from the outside it looks like the system working.
The screen is populated. The numbers are round. Nothing is red.

It is the bug where **a dashboard renders "I don't know" as a number**.

## Three different nothings

Take a single row on a monitoring screen: one site, one kind of check, one
count of open incidents. That count can be absent for at least three distinct
reasons, and they are not the same reason.

- **Nobody has asked yet.** The request is in flight, or the screen was drawn
  before the query returned.
- **The question cannot be answered here.** The endpoint does not exist in this
  deployment, or the caller has no permission to ask it.
- **The answer is genuinely none.** It was asked, it was answered, and the
  answer is that there are no open incidents.

Only the third of these is a zero. The other two are the absence of a fact.
And the distance between them is the whole value of a monitoring product: one
means *everything is fine*, and the other two mean *you have learned nothing,
and you should not go back to bed yet.*

## Why it always collapses the same way

The collapse is almost never a decision. It is a default.

A column is nullable in the database. A field is `Option<i64>` in the API. It
becomes `number | null` in the client. And then somewhere near the end, in a
template, someone writes the most reasonable-looking line of code in the file:

```
{{ openIncidents ?? 0 }}
```

Every step in that chain preserved the distinction faithfully. The last one
threw it away, in five characters, to avoid rendering an empty cell.

The same pressure applies at every layer. A schema wants a `NOT NULL DEFAULT
0` so that queries are simpler. An aggregation wants `COALESCE` so that a sum
is not poisoned by a missing row. A chart library wants a number so that a line
can be drawn. Each of these is locally sensible. Together they manufacture
confidence that nobody ever measured.

## What it costs

The cost is not the wrong pixel. It is that the wrong pixel is *reassuring*.

A missing value drawn as a zero is not a neutral error; it always fails in the
same direction. Unknown uptime becomes 0% — or, more often, an unknown *outage
count* becomes zero outages, which reads as perfect health. A certificate whose
expiry could not be fetched shows no warning, which is indistinguishable from a
certificate that is fine. A check that has never run reports no failures, which
is exactly what a passing check reports.

Every one of those is a screen that answers "is my site up?" with a confident
yes on the strength of no evidence whatsoever. If you are going to be wrong,
being wrong in the direction of "go and look" is survivable. Being wrong in the
direction of "everything is fine" is the failure people buy monitoring to
prevent.

## The rule we hold ourselves to

Inside HarpyWatch this is written down as three sentences, and they are
load-bearing enough that they sit at the top of the codebase's own
documentation:

> Missing data never renders as healthy. Stale never renders as fresh. And a
> fixture must never answer a question about a real fleet.

In practice that means a handful of things that are slightly annoying to build
and worth it every time:

**A limit of `null` is unlimited. A usage of `null` is unmeasured.** These are
opposites, and both of them are the absence of a number. A plan with no cap on
monitors and a plan whose usage we failed to read must not look the same.

**An unchecked bucket draws hollow, not green.** On an uptime timeline, a
period with no result is a gap. It is not filled in, and it is never
interpolated across — the line does not connect the two known points through a
region nobody observed, because the resulting picture is a claim about time
that was never measured.

**A result past twice its expected interval is stale, and says so.** A check
that should run every minute and last ran forty minutes ago is not reporting
"up". It is reporting "up, forty minutes ago", which during an incident is a
completely different sentence. So it is dimmed, it is marked, and nothing
derived from it is asserted.

**A number the screen has not asked for refuses rather than guesses.** There is
a counter in our own product's top bar that returns nothing at all on a build
whose API cannot answer it, rather than showing the fixture's zero. An empty
space with a reason beside it is honest. A zero is not.

## The awkward part

All of this makes screens uglier. Hollow cells, dimmed rows, em dashes where a
figure would look tidier, and the occasional sentence explaining why a panel is
empty. There is a real design cost, and the pressure to smooth it over is
constant and comes from good instincts — nobody sets out to build a dashboard
full of holes.

But a monitoring tool's only product is a true statement about someone else's
system. A gap in that statement is information. It says: *here is a thing we
did not observe, and you should decide whether that matters.*

Filling the gap in with a zero does not remove the uncertainty. It just moves
it from the screen, where somebody could act on it, to three o'clock in the
morning, where they cannot.
