When Things Go Wrong — Reviewing the App’s Fallbacks

Diagram of an app protected by encryption, access control, error handling, validation, and security shields

Stage 8 of codename Cosy Reader was about hardening. Accessibility audit. i18n — every user-visible string through a translation key, none hardcoded. Edge case review. And then something that I knew needed doing but had not done systematically: a full pass over the app’s error handling and fallbacks.

This is the post about that.

A fallback is not a try/catch that swallows the error silently. Catching an exception and logging it to the console is not handling it — it is hiding it. The user still sees whatever broken state the app landed in. They just have no way to understand why.

I went through the codebase looking for four categories of failure.

The first is network failures. The app already had a typed OfflineError — a subclass thrown when connectivity drops during a sync — so the chapter list could detect it and show a quiet notice rather than an angry error screen. What I found was that the same classification was not being applied consistently. When adding a new blog, if the connection dropped after WordPress was detected but during the actual content fetch, the user would see the generic “Something went wrong” message instead of the network-specific one. One additional try/catch and an isNetworkError() check fixed it.

The second is database failures. Here I found something worse. If the database failed to open on startup — full storage, corrupted file, hardware fault — the app would simply hang on the splash screen forever. The initDatabase() call had a .catch(console.error) that silently swallowed the failure and never set the ready flag. The splash never hid. The user had no idea what was happening. The fix was to catch the error properly, hide the splash, and show a calm screen: “Could not open your library. Try restarting the app.” Plain language. Honest. Actionable.

The third is corrupt or incomplete data. This one is subtle. When content is loaded from the database, the content_blocks column is parsed from JSON. The parse was already wrapped in a try/catch that returned an empty array on failure — good. But I had not checked whether the parsed value was actually an array. JSON.parse(‘null’) does not throw. It returns null. And null.map() does throw, later, in the reader, in a way that produces a blank screen with no explanation. One Array.isArray() check at the boundary fixed it. The reader already had a calm “no content available” message for the empty case.

The fourth is broken remote assets. Images in posts come from external URLs. Those URLs can 404, move, or fail to load. Without an onError handler on the Image component, a broken image shows a permanently grey box — the same as the loading placeholder. There is no way to tell whether the image is slow or missing. I added an onError handler that swaps the grey box for a labelled fallback: the image’s alt text if it has one, or a quiet “Image could not be displayed” if not.

Then there was one more thing I had been meaning to look at: error text. The messages across the app were consistent but I wanted to be sure they were actually useful. The test I used: does this message tell the person what happened, and does it give them something they can try?

“No connection — showing cached chapters.” Clear. The person knows why.

“Not available offline. Connect to the internet and pull to refresh from the chapter list.” Clear. Actionable.

“This site is not supported yet.” Honest without blame.

“Could not open your library. Try restarting the app.” Plain. Specific.

“Something went wrong. Please try again.” Generic — but only shown when there is genuinely no more information. Used sparingly now.

The full review found four real bugs and fixed all of them. None of them were dramatic. Each one would have produced a confusing experience for a real person in a real situation — low battery, patchy signal, full storage — and none of them would have produced a useful error message without the fix.

The app is now, I think, genuinely graceful under failure. Not just functional when everything works.

That feels like a meaningful milestone.

Leave a Reply

Discover more from Quiet Builds

Subscribe now to keep reading and get access to the full archive.

Continue reading