Error Handling Best Practices
How an application handles the unexpected says more about its reliability than how it handles the expected — good error handling is what separates a graceful failure from a confusing crash or, worse, silent incorrect behavior.
Last updated 2026-09-08
Catch errors at the level where you can meaningfully handle them, not everywhere
Wrapping every single function call in its own try/catch, often just to log and re-throw, adds noise without adding value. Catch where you can actually do something useful — retry, show a fallback, or translate the error into a user-facing message.
Never swallow errors silently
An empty catch block, or one that only logs to a console nobody's watching, hides real problems until they surface as a much harder-to-diagnose downstream symptom. At minimum, log with enough context to actually debug later.
Use custom error classes for distinct error categories
Distinguishing a validation error from a not-found error from a network error, using distinct error types, lets calling code handle each category appropriately rather than parsing an error message string to figure out what went wrong.
Include enough context in error messages to actually debug the problem
A generic 'Something went wrong' error, with no indication of what operation failed or what input caused it, forces whoever's debugging to reconstruct context that was available at the point of failure and then discarded.
Distinguish between operational errors and programmer errors
An operational error (invalid user input, a network timeout) is expected and should be handled gracefully. A programmer error (a genuine bug, like calling a function with the wrong argument type) usually shouldn't be caught and silently continued past — it should surface loudly during development.
Common Mistakes to Avoid
- ⚠Empty catch blocks that silently swallow errors with no logging
- ⚠Generic error messages with no context about what operation or input actually failed
- ⚠Catching errors at every function level instead of where they can be meaningfully handled
- ⚠Treating every error the same way regardless of whether it's expected (bad input) or a genuine bug
- ⚠Logging errors to a console that nobody actually monitors in production
Frequently Asked Questions
Should I always retry a failed operation?
Only for errors that are genuinely likely to be transient (a network blip, a rate limit) — retrying a request that failed due to invalid input or a genuine bug just repeats the same failure, wasting time and potentially compounding the problem.
Is it bad practice to throw errors, or should I return error objects?
Both are legitimate patterns with different tradeoffs — throwing integrates naturally with try/catch and async/await, while returning explicit result/error objects (common in some functional-style codebases) makes error handling visible in the type signature. Consistency within a codebase matters more than which one is 'correct.'