Error Handling That Actually Helps
We've all seen it: "An error occurred. Please try again later." It tells you nothing. You don't know what went wrong, whether trying again will help, or what to do next. It's the error handling equivalent of shrugging.
Good error handling is an investment that pays off every time something breaks, which in software is constantly. Let's talk about how to do it right.
The Two Audiences for Errors
Every error has two potential audiences: developers and users. They need completely different information.
Developers need technical details: stack traces, variable states, request IDs, timestamps. The more context, the faster they can debug.
Users need clarity: what went wrong in plain language, whether it's their fault or yours, and what they should do next. They don't want stack traces.
The mistake most people make is serving only one audience. They either show users cryptic technical errors, or they sanitize everything so much that debugging becomes impossible.
Structured Error Logging
When an error happens in your backend, log everything useful:
- A unique error ID (so you can correlate with user reports)
- The error message and stack trace
- The request that triggered it (sanitized of sensitive data)
- Relevant context: user ID, account state, feature flags
- Timestamp with timezone
Use structured logging with JSON format so you can actually search and filter your logs. Strings buried in paragraphs of text are useless when you're trying to find patterns.
Something like:
{"level":"error","error_id":"abc123","message":"Payment failed","user_id":"user_456","amount":99.99,"provider":"stripe","error_code":"card_declined","timestamp":"2025-10-03T14:30:00Z"}
User-Facing Error Messages
For users, every error message should answer three questions:
- What happened? (In plain language)
- Why did it happen? (If you know)
- What should they do? (The next step)
Instead of "Error 500," try: "We couldn't save your changes because our server is having issues. Your work hasn't been lost. Please try again in a few minutes, and if the problem continues, contact support with reference #abc123."
That message tells users it's not their fault, their data is safe, what to try next, and how to get help if needed. It treats them like humans instead of leaving them confused.
Error Categories Matter
Not all errors are equal. Categorize them so you can handle each type appropriately:
Validation errors are the user's mistake. Be specific about what's wrong and how to fix it. "Email address is invalid" is better than "Invalid input." "Email must contain @ symbol" is even better.
Authentication errors need to balance security with usability. Don't say "User not found" vs "Wrong password" since that helps attackers. But do say "Invalid email or password. If you forgot your password, you can reset it here."
Business logic errors explain constraints. "You can't delete this project because it has active tasks. Archive or complete the tasks first, then try again."
System errors are your fault. Apologize, reassure them their data is safe if true, and give them a path forward.
Fail Fast, Fail Explicitly
Don't let errors silently propagate until they cause bigger problems. Check for error conditions early and fail with clear messages.
If a function requires a non-null argument, check it at the start and throw immediately. Don't let it fail three function calls deep with a confusing NullPointerException.
If an API response is missing required fields, catch it right away. Don't let your UI crash because it expected data that wasn't there.
Error Boundaries in Frontend
In frontend apps, error boundaries prevent one broken component from taking down your entire UI. React, Vue, and other frameworks have built-in patterns for this.
Instead of a white screen of death, users see a graceful fallback. Maybe just that one widget is broken while the rest of the page works fine. Or they see a friendly "Something went wrong" with a refresh button instead of nothing at all.
Don't Swallow Errors
This is maybe the most common mistake: catching an error and doing nothing with it.
try { doThing() } catch (e) { /* ignore */ }
Every swallowed error is a future debugging nightmare. If you're catching an error, either handle it properly, log it, or at least add a comment explaining why you're ignoring it.
If you truly expect and want to ignore certain errors, be explicit: catch that specific error type and document why it's safe to ignore.
Testing Error Paths
Most people only test the happy path. But error paths need testing too. What happens when the API times out? When the database connection drops? When the user submits garbage data?
Write tests that deliberately trigger errors and verify your handling works correctly. Mock services to simulate failures. Your error handling code only works if it actually runs.
Monitoring and Alerting
Errors in production should alert someone. Tools like Sentry, Bugsnag, or Rollbar catch client-side and server-side errors automatically and group them intelligently.
Set up alerts for error rate spikes. A sudden increase in 500 errors probably means something just broke. The faster you know, the faster you can fix it.
Make Errors Actionable
The ultimate test of good error handling: when something breaks, can someone quickly understand what went wrong and fix it? If the answer is no, your error handling needs work.
Invest the time to write clear error messages. Your future self, your teammates, and your users will all thank you.