Telling the user what actually went wrong
When a request failed, our customer area would show a developer's error text, a spinner that never stopped, or an empty page that looked like the customer had no policies at all. I replaced all of it with one honest answer.
Every app fails sometimes. What matters is what it says when it does.
Ours said very little. A failed request could show a developer's error text, or spin forever, or — worst of all — render an empty page that looked exactly like "you have no policies". The customer had no way to tell whether their data was gone, their internet was down, or we were broken.
Every failure looked different
The customer area — where people see their policies, claims and documents — had grown one screen at a time, and each screen had invented its own way of coping:
- Some swallowed the error entirely and drew an empty list, quietly telling people their policies had disappeared.
- Some printed whatever the server said, including internal plumbing text never meant for a customer to read.
- Almost none offered a way to simply try again.
And the app couldn't tell apart three completely different situations: the customer's phone has no signal, one of our partner systems is offline, or the customer typed their passport number wrong. All three looked identical on screen.
Sorting failures into nine kinds
I wrote a single piece of code that every request now passes through. Whatever comes back — a timeout, a refused connection, a crash, a rejected form, or one of the several different error shapes the libraries we use like to throw — it becomes one consistent thing, labelled with one of nine kinds: offline, timed out, no connection, partner system down, our server broke, not found, invalid input, session expired, or unknown.
From then on the rest of the app only asks one question: what kind? And one component draws the answer, in three sizes — a full card for an empty page, a line under a form field, and a thin banner across the top.
Never show a developer's message
The rule I settled on is simple: the raw text from the server never reaches the screen. It is kept, but only for the console and the support line. What the customer reads is text we wrote ourselves, in their own language.
There are two deliberate exceptions and one addition:
- When the server is talking to the customer, let it. If they entered an impossible date, or the policy genuinely doesn't exist, that message is for them and is shown as it came.
- A code line for support. Real failures show a short reference like
502 · upstream unavailable, so an agent can tell in one second whose system broke. It's hidden when the cause is just a typo — nobody needs an error code for that. - One tap to try again. Every error card carries a retry button that re-runs exactly the request that failed, so a passing glitch costs a tap instead of a reload.
The server got the matching half: it now sends a short machine-readable code with every error, and internal gateway wording is stripped out before the response goes anywhere.
Why I'm proud of it
It landed as a single change across 43 files, and it's the kind of work that doesn't demo well — nothing new appears on screen. Twelve screens and every request in the customer area moved over in one pass.
What it removed was a whole class of quiet lies. An app that says "your session ended, log in again" instead of showing a blank page isn't really a technical improvement. It's the difference between a customer trusting what they see and calling to ask whether their insurance still exists.