Error Messages Are User Interface
Most error messages are written for the developer who wrote them, not the person who’ll read them. This is a problem.
When something goes wrong, the error message is the interface between your system and a confused, frustrated human. It deserves as much design attention as any other UI element.
The Anatomy of a Bad Error Message
You’ve seen these:
Error: Operation failed
Exception in thread "main" java.lang.NullPointerException
at com.example.Thing.doStuff(Thing.java:47)
Something went wrong. Please try again later.
What do these have in common? They tell you what happened (sort of), but not why it happened or what to do about it.
They’re written from the system’s perspective, not the user’s perspective.
What Good Error Messages Do
A good error message answers three questions:
- What happened? In terms the reader can understand.
- Why did it happen? The proximate cause, not the technical details.
- What can they do about it? Concrete next steps.
Compare:
Bad: Error: Connection refused
Better: Could not connect to the database at localhost:5432. Is the database server running?
Best: Could not connect to the database at localhost:5432. The connection was refused, which usually means the database server isn't running. Try: sudo systemctl start postgresql
The best version takes a few more bytes, but it saves the user from a Stack Overflow search.
Context Is Everything
The same underlying error might need different messages in different contexts.
A file not found when reading config at startup: “Configuration file /etc/myapp/config.yaml not found. Create this file or specify an alternate path with –config.”
A file not found when processing user input: “The file ‘report.csv’ doesn’t exist. Check that you typed the filename correctly.”
A file not found in an internal operation: This one can stay technical — it’s a bug, and the developer needs the details.
The message should match the audience and the situation.
The Logging vs. Displaying Distinction
Not every error should be shown to users. And the error you show users should rarely be identical to what you log.
Log: Full stack traces, system state, request IDs, timing information. Everything a developer needs to reproduce and fix the issue.
Display: Clear explanation, actionable guidance, maybe a reference ID to connect user reports to logs.
These serve different purposes. Conflating them serves neither well.
Error Message Design Patterns
Include the failed value when relevant:
- Bad: “Invalid date format”
- Good: “Invalid date format: ‘2026-13-45’. Expected format: YYYY-MM-DD”
Suggest common fixes:
- Bad: “Permission denied”
- Good: “Permission denied writing to /var/log/myapp.log. Check that the log directory is writable by the current user.”
Be specific about what was attempted:
- Bad: “Network error”
- Good: “Failed to fetch user profile from api.example.com: connection timed out after 30 seconds”
Provide escape hatches:
- “If this error persists, you can bypass this check with –skip-validation (not recommended for production)”
Common Mistakes
Being too terse. Disk space is cheap. Network bandwidth is cheap. The user’s time is expensive.
Being too verbose. Burying the key information in paragraphs of text defeats the purpose.
Hiding technical details entirely. Sometimes users need them — to report bugs, to search for solutions, to understand what went wrong.
Assuming expertise. “ECONNRESET” means something to a systems programmer. “The connection was unexpectedly closed” is clearer to everyone.
Showing scary errors for normal situations. If a user cancels an operation, don’t show an error. If a network hiccup is recoverable, don’t make it look fatal.
The “Should Never Happen” Trap
Every codebase has comments like // this should never happen followed by an empty error message or a generic exception.
Then it happens. In production. On a Friday evening.
If you’re handling an “impossible” case, take the extra minute to write a useful message. Future you will be grateful.
# Instead of:
raise Exception("Unexpected state")
# Write:
raise Exception(
f"Unexpected state '{state}' in process_order. "
f"Expected one of: {VALID_STATES}. "
f"Order ID: {order_id}. This may indicate database corruption."
)
Testing Error Messages
Error messages are features. They should be tested.
Does the message actually appear when the error condition occurs? Is it helpful? Does it contain the right context?
I’ve caught bugs where the “helpful” error message referenced the wrong variable, or where the suggested fix didn’t actually work for the described condition.
Error Messages as Documentation
Good error messages reduce support burden. They reduce time spent debugging. They help users help themselves.
They’re also a form of documentation — they describe the boundaries of what the system expects and handles.
When I’m learning a new tool, the quality of its error messages tells me a lot about the quality of the tool overall. Systems that fail gracefully and informatively are usually well-designed in other ways too.
The Investment
Writing good error messages takes time. More time than throw new Error("failed").
But that time is paid back every time someone hits that error and knows what to do next. Every support ticket not filed. Every hour not spent debugging.
Errors will happen. Make them useful.