Advertisement

New developers write the happy path — the version where every input is valid, every network call succeeds, every file exists. Experienced developers spend a disproportionate share of their attention on everything that can go wrong, because that is where real software lives or dies. The difference between a demo and a product is largely error handling.

A few principles separate error handling that helps from error handling that hides problems.

Fail loudly, not silently

The worst error handling is the kind that swallows problems — catching an error and doing nothing, so the program limps on in a broken state and fails mysteriously much later, far from the cause. If you cannot meaningfully handle an error where it occurs, it is usually better to let it surface than to bury it.

A silent catch is a debt that comes due at the worst possible time, with no clue about where it started.

Advertisement

Handle it at the right level

Not every function should handle every error. Low-level code often should just report a failure and let a higher level — one that has enough context to decide what to do — handle it. A file-reading function does not know whether a missing file should retry, warn the user or abort; the caller does. Pushing decisions to where the context lives keeps code both simpler and more correct.

The art is choosing that level deliberately, rather than wrapping everything in a reflexive try/catch that pretends to cope.

Give good information

When an error is reported or logged, it should carry enough context to be actionable: what was being attempted, with what inputs, and what failed. "Something went wrong" wastes the future debugger’s time; a specific, contextual message can save hours. This applies to messages shown to users too, which should be clear and non-alarming without leaking internals.

Good error handling is really about honesty: acknowledging that things fail, surfacing failures where they can be understood, and leaving a trail that makes the next person’s job possible.

Advertisement

Exceptions versus result types: two fundamentally different control-flow philosophies

Languages built around exceptions treat an error as an interruption to normal control flow, propagating automatically up the call stack until something catches it, which keeps the common, successful path free of explicit error-checking clutter but makes it easy to forget that a given call can fail at all, since nothing in a function's own signature necessarily signals it. Languages and libraries built around result types instead make failure an explicit part of a function's return value — a `Result<T, E>` or a tuple of `(value, error)` — forcing every caller to at least acknowledge the possibility of failure at the call site, at the cost of more verbose code for the common success path.

Why swallowing an error silently is worse than crashing loudly

A catch block that does nothing — or worse, one that logs nothing and simply continues — converts a genuine failure into invisible, silently incorrect behavior that can propagate far from its actual cause before anyone notices anything is wrong at all; a program that crashes loudly the moment an unhandled condition occurs is, perversely, easier to debug and often safer than one that presses on with corrupted or partial state after silently discarding evidence that something already went wrong.

Advertisement

Retrying versus failing fast: not every error deserves the same response

A transient network blip warrants a retry, ideally with the backoff strategy discussed elsewhere in this library; a malformed request or a genuine logic bug does not — retrying an operation that will deterministically fail again every single time wastes effort and delays surfacing a problem that actually needs a code fix, not another attempt. Distinguishing between these two categories, transient and permanent, at the point an error is first caught, rather than applying one blanket retry policy to every kind of failure indiscriminately, is what separates resilient error handling from error handling that merely looks resilient on the surface.

Why beginners under-handle errors and experienced engineers sometimes over-handle them

A beginner's code often assumes every operation succeeds, because most tutorials demonstrate only the happy path and skip failure cases entirely — the natural overcorrection, once that gap becomes painfully apparent from a production incident, is wrapping everything in defensive error handling regardless of whether a given failure is actually meaningfully recoverable at that specific point in the code; the mature middle ground handles errors deliberately at the layer that can actually do something useful about them, letting errors that cannot be meaningfully handled at a given layer propagate cleanly to one that can, rather than catching indiscriminately at every single layer just because catching feels safer.

Why error messages meant for developers and messages meant for users need to stay separate

A raw stack trace or an internal error code is exactly the right level of detail for a developer debugging a failure, and exactly the wrong thing to show an end user, who needs a clear, actionable, non-technical explanation instead — conflating the two, either exposing internal details to users or dumbing down every internal log message to user-friendly prose, serves neither audience well, and mature error handling maintains two genuinely separate representations of the same underlying failure, one for each audience.

Why error handling code is disproportionately undertested relative to its risk

Happy-path code gets exercised naturally just by using the application normally, while error-handling code only runs when something has actually gone wrong, which means it is exactly the code least likely to be accidentally exercised during ordinary manual testing and most likely to contain an undiscovered bug of its own — deliberately writing tests that force specific failure conditions, rather than relying on error paths to be incidentally covered by testing the success case, is what actually verifies that error handling works correctly rather than merely existing and looking plausible.

Why a global error handler is a safety net, not a substitute for local handling

A top-level catch-all error handler that logs and gracefully responds to any otherwise-uncaught error is valuable specifically as a last line of defense against something nobody anticipated, but relying on it as the primary error-handling strategy rather than a backstop misses the chance to handle a specific, expected failure meaningfully closer to where it actually occurred — a global handler can log an error and return a generic failure response, but only code closer to the actual failure knows whether a specific, more useful recovery (a fallback value, a retry) is actually possible.

Why an error's context should travel with it, not just its message

An exception re-thrown with only its original message, stripped of the specific input values or state that triggered it, forces whoever debugs it later to reconstruct that missing context from scratch — attaching relevant contextual data directly to an error object as it propagates, rather than discarding it at each level the error passes through, preserves exactly the detail a later investigation is most likely to need and least likely to be able to recover any other way.

Why 'fail fast' and 'graceful degradation' are not actually contradictory philosophies

Failing fast on a genuine programming error — an invalid internal state that should never occur — and degrading gracefully on an external, expected failure — a downstream service being temporarily unavailable — are both correct responses applied to two different categories of failure, and confusing the two produces the wrong behavior in both directions: gracefully working around a genuine bug hides it, while failing hard on a merely temporary external outage is needlessly brittle where a fallback would have served users far better.

Why typed errors in a strongly typed language catch mistakes a generic exception cannot

Throwing a generic, untyped exception for every kind of failure means a catch block has no compile-time way to know which specific failures it might actually need to handle, while a language supporting typed or sealed error hierarchies lets the compiler verify that every expected error case has actually been handled somewhere — this shifts a category of 'forgot to handle this specific failure mode' bug from a runtime surprise to a compile-time error, catching it well before the code ever ships rather than discovering the gap from a real, unhandled production incident.

Why error handling should be designed alongside the happy path, not bolted on afterward

Designing a function's success behavior first and only afterward considering what could go wrong tends to produce error handling that feels retrofitted and incomplete, covering only the failure modes that happened to occur to the author after the fact — designing both together from the start, explicitly enumerating what can fail before writing any implementation at all, produces more thorough, more deliberately reasoned error handling than treating it as a secondary concern addressed only once the main logic already exists.

Why centralizing error-message copy separately from error-handling logic pays off

Scattering user-facing error message strings directly inline throughout error-handling code makes them hard to keep consistent in tone and hard to localize later, while centralizing them in one dedicated location, referenced by error code from wherever the actual handling logic lives, keeps the message content and the handling logic as two separately maintainable concerns, which matters increasingly as an application grows and needs its error messages to stay consistent across many different call sites.

Why error handling deserves its own dedicated section in an incident postmortem

A postmortem that only asks what caused the original failure, without also asking whether the error handling around it behaved as intended — did it fail loudly or silently, did it retry sensibly or not at all — misses half of what is actually worth learning from an incident, since the quality of the surrounding error handling is often what determined whether the original failure stayed a minor blip or escalated into a genuine outage.

Why the best error handling is invisible to the vast majority of users, most of the time

The goal of everything covered in this article is not primarily impressive-looking error screens, it is a system resilient enough that most failures are absorbed, retried, or gracefully worked around before a user ever notices anything went wrong at all — visible, well-designed error messages matter for the failures that do surface, but the measure of genuinely mature error handling is how rarely users ever have to see one in the first place.

Why error handling conventions deserve the same team-wide documentation as coding style

A team where each engineer independently decides how to structure error handling produces a codebase with as many different error-handling conventions as there are engineers who have touched it, which makes every new piece of code harder to reason about consistently — documenting a shared team convention for exceptions versus result types, logging format, and retry policy, the same way a team documents its coding style guide, keeps error handling as predictable across a codebase as any other shared convention.