AppCrumble mascotAppCrumble
DesignErrorStateCopywritingUX

Your Error Messages Are Talking to the Wrong Person

"Error 500" is a note to a developer, taped to the one screen a confused customer is looking at right now. Here's who your copy should actually be written for.

September 3, 20266 min read

"Error: ECONNREFUSED at 0x7ffcafd012a3" is a perfectly reasonable thing to write to a log file. It is not a reasonable thing to put in front of someone who just wanted to update their billing address. Most bad error messages aren't wrong, exactly - they're just addressed to the wrong reader.

Two audiences, one screen

Every error has a developer audience and a user audience, and they want almost opposite things. The developer wants the stack trace, the request ID, the exact failing assertion - context for fixing the bug later. The user wants none of that. They want to know if it's their fault, whether trying again will help, and what to do if it won't. Writing one message for both usually means the developer's version wins, because it's the one that was easiest to throw on screen from a catch block.

Not all errors deserve the same response

Lumping every failure into one generic "Something went wrong" banner treats a dropped Wi-Fi connection the same as a permanently deleted resource, and that's where most error copy quietly stops being useful:

  • Recoverable, no user action needed. A timed-out request or a rate limit. Safe to retry automatically, or offer a single "Try again" button - inviting a retry that's actually likely to succeed.
  • User-fixable. A validation failure - an invalid card number, a required field left blank. Point at the specific field instead of a page-level banner; the fix belongs exactly where the mistake was made.
  • Unrecoverable. A permission denial, a resource that's been deleted. A "try again" button here is actively misleading - it will fail the same way every time. State the dead end plainly and offer a way out, like a link back to somewhere that still works.

A message in three parts

  • What happened, in plain language - "we couldn't reach the server," not "request failed."
  • What to do about it, matched to which of the three categories above it falls into.
  • The technical detail, tucked away - a small error code or a "Show details" disclosure, there for the one person in a thousand who needs it, invisible to everyone else.

Give support a paper trail

A friendly sentence and a stack trace aren't the only two options. A short, human-readable reference code - shown alongside the plain-language message and logged server-side with the full context attached - lets a confused user describe their problem precisely without seeing a wall of technical noise: "Something went wrong on our end. Reference: ERR-8F2A19." Support (or you, at midnight) can search the logs for that exact string and land on the full trace in one query, instead of asking "can you send a screenshot?" and hoping the browser console is still open.

Make sure it's actually announced

A red banner that appears silently is invisible to anyone using a screen reader unless the markup says otherwise. role="alert" (which carries an implicit aria-live="assertive") tells assistive technology to interrupt and announce the message the moment it appears, the same way a sighted user's eye is drawn to it immediately. Skip it, and the error is only half-shown - present in the DOM, absent from the experience.

Note
A quick test: if support could resolve the ticket faster because the error message told the user something useful, the message is doing its job. If support's first reply is always "can you send a screenshot of the error," the message isn't saying enough.

AppCrumble's ErrorState component keeps the split explicit in its props - a headline and an action are required, a technical detail is optional and rendered smaller, below the fold of attention. It's a small constraint, but it's the one that keeps a caught exception from turning into a support ticket titled "the app is broken."