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.
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.