Most code is written in the happy path first and the failure paths second, which is reasonable — you have to know what success looks like before you can define what falling short of it means. The trouble is that the second part often gets treated as mechanical cleanup rather than design: the network call might fail, so wrap it in a try; the value might be absent, so return null; the write might conflict, so log something and move on. Each of these is a real decision about how the system behaves in a situation that will definitely occur, made in a few seconds by someone whose attention is on the success case. And what a system does when things go wrong is not a footnote to what it does — for the people relying on it, it’s frequently the most consequential part.

Consider how much is actually being decided in a single catch block. Whether the operation is retried or abandoned. Whether the user learns anything about what happened, and whether what they learn is actionable or just alarming. Whether partial work is rolled back or left half-applied. Whether the failure propagates to the caller, who has context to decide, or gets swallowed here, where it’s convenient. Whether the system stays available in a degraded state or refuses to proceed. These are architectural questions with very different consequences, and they’re routinely settled by whatever pattern was nearest to hand when the compiler complained about an unhandled case.

The reason this happens is that failure paths lack the feedback that shapes everything else. The happy path gets exercised constantly — by you while developing, by tests, by every user every day — so its rough edges surface fast and get filed down. The failure path gets exercised rarely, often only in production, often by someone who isn’t in a position to report back well. So the error handling nobody thought hard about stays unexamined for a long time, and its wrongness only becomes visible during an incident, when the cost of that wrongness is at its highest and there’s no time to reconsider the design. Failure paths are the least-tested, least-observed, highest-stakes code in most systems.

What helps is treating the error case as a spec question rather than a code question — asking, before writing the handler, what should happen here. Not “how do I make this compile” but “what does the person on the other end need when this fails?” Sometimes the answer is retry silently, because the failure is transient and nobody needs to know. Sometimes it’s fail loudly and immediately, because proceeding would corrupt something. Sometimes it’s degrade — serve stale data with a note rather than nothing at all. Sometimes it’s propagate, because the caller knows things this layer doesn’t. These are genuinely different answers, and which one is right depends on the meaning of the operation, not on the mechanics of the language. The handler should follow from that decision, not substitute for it.

None of this requires exhaustive contingency planning for every conceivable fault. It’s a shift in how much attention the failure branch earns relative to the success branch — closer to equal than most code reflects. The happy path is what your software does when the world cooperates, which is a decreasing fraction of the time as a system grows and takes on dependencies. The failure paths are what it does the rest of the time, and they deserve to be designed rather than defaulted into. This opens a short series on failure: what to do when things break, what to tell people about it, and why the choices there are more visible than they look.