Error messages are a user interface
The message someone reads at their worst moment gets less design attention than a button. Here's what a good one contains.
An error message is read by a person who is already frustrated, under time pressure, in an unfamiliar state. It is the highest-stakes text in your product and it typically receives less design attention than a tooltip.
the anatomy of a good one#
Four parts. Most messages have one.
What happened, specifically.
Not "an error occurred." What failed, in terms the reader can act on.
Why, if you know.
The immediate cause. Not the stack trace — the reason.
What to do next.
The single most valuable part and the most often missing. Even "retry in a few minutes" is infinitely better than nothing.
How to get help.
An error code, a request ID, a link. Something the reader can paste into a support conversation that makes the problem findable.
the transformation#
Error: ECONNREFUSEDversus
Could not connect to the database at db.internal:5432.
The connection was refused, which usually means the database is not
running or a firewall is blocking the port.
Try:
1. Check the database is running: systemctl status postgresql
2. Verify the host and port in config/database.yml
3. Check network access: nc -zv db.internal 5432
If this persists, include request ID a1b2c3d4 when reporting.The second is longer, and length is not the cost people think it is. Nobody has ever complained that an error message told them too much about how to fix their problem.
the rules#
Never blame the user. "Invalid input" is accusatory and unhelpful. "Email address must contain an @ — you entered dom.example.com" is neither.
Echo what they gave you. The single most useful thing a validation message can do is show the value that failed. The user's mental model of what they typed is frequently wrong, and showing it resolves the confusion instantly.
Be specific about which one. "One or more fields are invalid" makes the user hunt. Name the field. Highlight it. If there are several, list all of them — do not make them fix one, submit, and discover the next.
Distinguish the four failure classes, because the correct user action is different for each:
| class | what the user should do |
|---|---|
| your fault (bug) | report it, with an ID |
| their fault (bad input) | fix the input, specifically |
| transient (network, load) | retry, after a stated interval |
| policy (not permitted) | request access, from a named place |
Collapsing these into one generic message means the user cannot tell whether to retry, fix something, or give up.
Include an identifier for the unfixable ones. When it is genuinely your bug, the user cannot fix it and the best thing you can give them is a request ID that appears in your logs. That converts "it's broken" into a support conversation you can actually resolve.
Never expose internals to end users, always expose them to developers. A stack trace on a public error page is an information disclosure. The same trace in a developer tool's console is essential. Know which audience you are writing for.
the developer-facing case#
For errors read by other engineers — library exceptions, CLI failures, API responses — the bar is different and higher.
Say what you expected and what you got.
expected `timeout` to be a positive integer (milliseconds), got: "30s"
Durations must be numeric. Use 30000 instead of "30s".Point at the source. File, line, and the offending fragment. Compilers have done this well for years and application errors mostly have not.
Link to documentation with an anchor to the specific section, not the homepage.
Give the error a stable identifier so people can search for it. E1042 is searchable in a way that a prose sentence you will reword next release is not.
Rust's compiler errors remain the standard here and the reason is that someone decided error messages were a feature and staffed them accordingly.
the audit worth running#
Grep your codebase for every string that gets shown on a failure. Read them as a list, out of context, the way a user encounters them.
You will find: generic messages, internal jargon, messages that describe the code's state rather than the user's situation, and at least one that says something like "this should never happen."
Fixing them is a day of work and it is the cheapest quality improvement available in most products.
the framing#
Every error message is a conversation with someone having a bad time. You get one sentence, maybe three, to help.
Write them like you will be the one reading them at 2 a.m., because eventually you will be.
— Dom, May 6, 2026