Skip to main content
Handbook/Harden/Page 95 · Reliability

Handling Failure Gracefully

Share

Share this page

Pass it to someone who needs it.

Star on GitHub

Key takeaway: Handle failure gracefully

Your app works every time you click through it yourself. That is the happy path, the run where nothing goes wrong. Real users hit the paths where something does, and an agent writes only the happy one by default. This chapter gets your app to stay trustworthy when the rest of reality shows up.

10.6.1Assume every call can fail

Every time your code reaches outside itself, that reach can fail, and eventually it will. Treat failure as a normal branch you handle, not a rare accident you ignore.

The three you hit first:

  • The network drops or times out mid-request.
  • The disk is full, or the file you expected is gone.
  • A service you depend on is down, or slow enough to look down.

Your agent assumes all three succeed. Your job is to assume they will not. Four defenses handle the reach that fails:

  • Set a timeout so a slow service cannot hang your app.
  • Retry a brief failure, with exponential backoff: wait longer each try.
  • Add jitter, a little randomness on each wait, so clients do not retry in lockstep.
  • If a service keeps failing, a circuit breaker stops calling it for a while so it can recover, and you fail fast instead of piling up.

10.6.2Catch the error, never swallow it

When a call fails, an agent often buries it: a catch that returns an empty value and moves on. The app does not crash, so it looks fine, but the failure vanished with no record and the user is left staring at nothing.

Handling an error means two things, always: log the real cause, a written record you can search later, and show the user a message they can act on.

Swallowing hides the failure; handling logs the cause and gives the user a message to act on.
// Swallowed: the failure vanishes and the // user sees an empty list, never knowing why. async function loadOrders() { try { return await api.getOrders() } catch { return [] } }
// Handled: log the real cause for you, show // the user a message they can act on. async function loadOrders() { try { return await api.getOrders() } catch (err) { logger.error('getOrders failed', err) throw new Error( 'We could not load your orders. Try again.' ) } }

Rule of thumb: every catch earns its place by doing two jobs, a log for you and a message for the user. An empty one is a silent failure you will hear about from a confused user instead.

10.6.3Cover the edge cases the agent skipped

An edge case is a condition at the boundary of what the code expects, the empty input or the huge one, not the tidy middle it was demoed on. Your agent tests the comfortable middle and skips the edges, so name them and hand them back.

Edge caseWhat breaks when you ignore it
Empty (no rows, blank field, zero items)The layout breaks, or the math divides by zero
Huge (a 2 GB file, 10,000 rows at once)The page freezes or the server runs out of memory
Concurrent (two people save the same second)One save silently overwrites the other
Malformed (letters where a number goes)The code crashes on the first bad value

One more the agent gets wrong by default is not an edge case but a storage decision: keep every timestamp in UTC and convert only at the moment you display it. Store local times instead and your first user in another country sees yesterday's data. The weekend the clocks change, some of it is simply wrong.

Later, QA turns these same cases into tests that re-check them on every change. For now, handling them by hand is the win.

10.6.4Leave data consistent when it breaks

The worst failure is the one that stops halfway. A checkout charges the card, then crashes before it saves the order: the money moved and the record did not. That half-written state is far worse than a clean crash, because nothing tells you it happened.

Group the steps that must all happen, or none, into one transaction. The database runs them as a single unit and undoes everything if any step fails, so a crash leaves the data untouched.

For steps outside the database, like charging a card or sending mail, make each one safe to run twice, so a retry never doubles the damage. That property is idempotency. An idempotency key, a token you send with the request, is how a provider makes a repeated call take effect only once.

A transaction runs all-or-nothing steps as one unit and undoes them if any step fails.

This prompt hardens one feature without changing what it does:

Ready prompt
Act as a senior engineer hardening one feature for reliability. Do not add features or change what it does. Read my NFR targets and my conventions first: timeouts, retries, and error wording must match the numbers I already committed to, not new ones you pick. Work through this feature and: 1. Find every call that can fail: network, disk, database, or another service. Make sure each is handled, not swallowed. 2. For each failure, log the real cause for me and return a message the user can act on. Flag every empty catch or silent default. 3. List the edge cases it ignores, empty, huge, concurrent, and malformed, and handle each one. Flag any timestamp stored in local time instead of UTC. 4. Find any write that happens in several steps. Wrap the steps that must all succeed in one transaction, so a mid-way failure leaves no half-written record. Give me the risks first as a short list, then the fix for each. Touch behavior only where it was unsafe. Add any standing rule you applied to my rules file, and note the change in my changelog. If you need the full reasoning behind this step, read https://zalt.me/guides/vibe-coding/harden/making-it-reliable The feature to make reliable:

Do this now: paste the prompt with your riskiest feature, the one that touches money, files, or another service. Let the agent turn its happy-path code into code that survives a failure.

Mahmoud Zalt

Mahmoud Zalt

Software engineer, 16+ yrs · built Sistava.com in 3 months, idea to production, using these methods

Resources
Star on GitHubContribute
Donate

Support my work

A small tip keeps the free work coming.

© 2026 Mahmoud Zalt. Free to read, not to republish.
Copyright & license