Kestrel Bank's app had a 2.4-star rating and a churn problem nobody could explain. Feature parity with competitors was strong. The retail team assumed the answer was more features.
We read 18,000 store reviews and 40,000 support tickets before touching the design. Sixty-eight percent of one-star reviews described three specific failure modes, and all three ended at the same screen: a grey illustration, the words Something went wrong, and a Retry button.
What that screen actually communicates
It says: we know this failed, we know why, and we have decided not to tell you.
That is an unusually insulting thing to say to someone checking whether their rent cleared. The user is not confused about whether something went wrong — they can see that. They want to know whether it is their fault, whether their money is safe, and whether pressing Retry will help or make it worse.
The generic screen answers none of those, and users correctly infer that the answer was available and withheld.
Why it exists
Not laziness. It accretes.
Early on there is one error state and it is genuinely unknown-unknown. Then a network timeout gets added to the same branch because it is 11pm before a release. Then an auth expiry, because the auth work is happening in a different squad. Then a 500 from a downstream service that nobody owns.
Within a year, one screen is catching six distinct conditions with six distinct correct responses, and it is showing the intersection of all of them, which is nothing.
The rule we applied
Every failure path gets a specific, recoverable message. If you cannot write one, that is a signal you do not understand the failure — which is the actual problem, and worth finding out about.
Concretely, on the Kestrel rebuild:
- Session expired → "You've been signed out for security. Sign in to continue." Plus a sign-in button that returns to where you were.
- Offline → The cached balance, timestamped: "Last updated 14 minutes ago." No error screen at all, because nothing has gone wrong; the app simply cannot refresh.
- Payments provider down → "Payments are temporarily unavailable. Your money is safe and nothing has been taken." That second sentence was the single most requested reassurance in the ticket analysis.
- Genuinely unknown → A message with a reference code and a one-tap path to support with that code attached.
That last one is the only survivor of the original screen, and it now appears in roughly 0.2% of error events rather than 100%.
Keeping it deleted
We removed the component and added a lint rule that fails the build if a string matching the old copy reappears. This sounds petty. It is not — that screen has re-entered every codebase I have removed it from, usually within two quarters, usually at 11pm before a release.
The lint rule is not really about the string. It is a forcing function that makes someone, at that moment, spend ten minutes deciding what the error actually means.
The result
Rating moved from 2.4 to 3.6 in the eleven weeks after the error-handling release, before any new feature shipped. It reached 4.7 over the following year. App-related support contacts fell 41%.
The retail team's instinct — that they needed more features — was reasonable and wrong. They needed fewer failures, and more specifically, they needed the failures they already had to be honest about themselves.