Skip to content

· 2 min read

An empty page is worse than an outage

When this site moved its content into a database, the first version degraded gracefully to nothing. That was the worst failure mode available, because nothing looked broken.

  • Engineering
  • Reliability
  • Architecture

When I moved this portfolio's content from files into a database, so it could be edited from an admin panel, the first version of the read layer did what a lot of code does: if the database could not be reached, it caught the error and returned an empty list.

It felt responsible. The site would "stay up". Then I thought about what "up" would actually look like.

What graceful degradation looked like

With the database unreachable, the site would render perfectly. The hero, the typography, the navigation, the motion — all present. The Selected Work section would simply have no work in it. The project universe would be empty. The page would return HTTP 200.

To a visitor, that is indistinguishable from a deliberate design: a portfolio with nothing in it. To a search engine, it is a page that has been updated to contain less. To me, it would look like a quiet day, because nothing would alert.

That is worse than an outage. An outage gets noticed and fixed. A plausible-looking empty page can sit there for days.

Two different events

The fix started with separating two things the original code had collapsed:

  • The database is reachable and has nothing in it. That is a legitimate state — before any content is added, for example — and it should render an empty state.
  • The database cannot be reached. That is a failure, and it should look like one.

So every content read now goes through a small guard. An empty result passes straight through. Anything that throws is logged as a structured event and rethrown as a specific error.

What failing loudly buys

The build refuses to ship an empty site. Because pages are pre-rendered at build time, a build that cannot reach the database now fails instead of succeeding. An empty portfolio cannot be deployed at all. I tested this directly: pointed a build at an unreachable database and confirmed it exits with an error.

Failures stay out of the cache. The read layer is cached. A cache stores whatever the function returns — including an empty list returned by mistake. Throwing means a transient blip can no longer be saved and served as the site's content until the next deploy.

There is a controlled error page. If a live request does fail, the visitor sees an honest "temporarily unavailable" message rather than an empty portfolio.

Health is reported separately. A dedicated health endpoint distinguishes "the app is running" from "the database is reachable", and returns 503 when it is not. That is what monitoring watches.

The general lesson

Every instinct during an incident says: keep the site up. Usually that is right. But "up" has to mean "serving the thing it exists to serve". A system that stays technically available while silently serving nothing has not degraded gracefully. It has failed in the one way nobody will notice.

When you write a fallback, ask what the user will actually see if it triggers — and whether anyone will ever find out.