"Error: Something went wrong."
Five words, zero information, and I'd bet you've felt the specific frustration of staring at a message like that with no idea what to do next — or, if you've shipped software long enough, you've written one yourself under deadline pressure and told yourself you'd fix it later. Bad error messages don't just frustrate users — they show up downstream as support tickets, abandoned workflows, and a quiet erosion of trust in the product.
The thing I've come to believe after looking at a lot of these: an error message is one of the few moments where your product talks directly to a user who's already stuck. Get it right and you've turned a bad moment into a small vote of confidence. Get it wrong and you've confirmed their worst assumption about your software.
A 2025 study by UserTesting found that 73% of users blame themselves when they encounter unclear error messages — they assume they did something wrong rather than recognizing the message failed them, which means they retry the same broken action instead of asking for help. That statistic alone is a good reason to take this more seriously than most teams do.
The Anatomy of a Perfect Error Message
The error messages that actually work share the same shape: they state what happened, they explain why if that's useful context, they tell the user how to fix it, and they leave an escape hatch for when the user can't fix it themselves. "Your password is incorrect" is the what. "Passwords are case-sensitive" is the why, when it's worth including. "Check your Caps Lock key and try again" is the how. "Forgot password? Reset it here" is the escape hatch. Not every error needs all four — a lot of good ones skip the why entirely — but every error needs the what and the how, at minimum.
Bad vs. Good: Real Examples
Example 1: Form Validation
| ❌ Bad | ✅ Good |
|---|---|
| "Invalid input" | "Email address must include @ and a domain (e.g., name@example.com)" |
| "Error in field 3" | "Phone number should be 10 digits. You entered 9." |
| "Validation failed" | "Password needs at least 8 characters, one number, and one symbol" |
Example 2: File Operations
| ❌ Bad | ✅ Good |
|---|---|
| "File error" | "Can't save 'report.pdf'—the file is open in another program. Close it and try again." |
| "Permission denied" | "You don't have permission to edit this folder. Contact your admin or save to a different location." |
| "Upload failed" | "This file is 25MB, but the limit is 10MB. Compress the file or upgrade your plan." |
Example 3: Network/API Errors
| ❌ Bad | ✅ Good |
|---|---|
| "Network error" | "Can't connect to the server. Check your internet connection and try again." |
| "Request failed" | "The server is temporarily busy. Your work is saved—try again in a few minutes." |
| "500 Internal Server Error" | "Something went wrong on our end. We've been notified and are working on it. (Error ID: #A7F2B)" |
Example 4: Authentication
| ❌ Bad | ✅ Good |
|---|---|
| "Login failed" | "The email or password you entered doesn't match our records. Try again or reset your password." |
| "Session expired" | "You've been logged out for security (inactive for 30 minutes). Sign in again to continue." |
| "Unauthorized" | "You need admin access to view this page. Request access from your team admin." |
Notice the pattern across all four: the bad version describes a system state ("failed," "expired," "unauthorized"), and the good version describes what the user should do next. That's really the whole discipline in one sentence — stop writing from the system's point of view and start writing from the point of view of someone who just wants to get back to work.
Tone and Language
A few language swaps do most of the work: "invalid" becomes "needs to be" or "should include," "failed" becomes "couldn't" or "wasn't able to," "illegal character" becomes "can't include [character] — try removing it." None of this is complicated, but it's the difference between a message that sounds like a system rejecting you and one that sounds like it's on your side.
I'd also skip exclamation marks entirely in error copy — "Error!" reads as alarmed, and alarm is rarely the right register for "you typed your phone number with a hyphen." Sentence case for headlines, full stops for complete sentences, and lowercase for generic technical terms unless they're genuinely proper nouns. These are small things, but a style guide that gets the small things consistent is one users stop noticing, which is exactly what you want from error copy — it should never be the interesting part of using your product.
Error Messages by Context
Some contexts are common enough that it's worth having a default template ready rather than reinventing the wording each time:
| Field Type | Error Template |
|---|---|
| "Enter a valid email address (e.g., name@company.com)" | |
| Password | "Password must be [X] characters with [requirements]" |
| Phone | "Enter a [X]-digit phone number" |
| Date | "Enter a date in [format] format (e.g., [example])" |
| Number | "Enter a number between [min] and [max]" |
| URL | "Enter a complete URL starting with https://" |
| Required field | "[Field name] is required" |
Timing for Validation
| Approach | When to Use | Example |
|---|---|---|
| On blur | Most fields | Show error when user leaves field |
| On submit | Complex forms | Validate all at once, scroll to first error |
| Real-time | Passwords, usernames | Show requirements as user types |
| Delayed | Search, autocomplete | Wait 300ms after typing stops |
The same logic applies to HTTP status codes — the code itself is meaningless to most users, so translate it:
| HTTP Code | User-Friendly Message |
|---|---|
| 400 | "We couldn't process your request. Check your input and try again." |
| 401 | "Please sign in to continue." |
| 403 | "You don't have permission to access this. Contact your admin if you need access." |
| 404 | "We couldn't find what you're looking for. It may have been moved or deleted." |
| 408 | "The request took too long. Check your connection and try again." |
| 429 | "You've made too many requests. Please wait a moment and try again." |
| 500 | "Something went wrong on our end. We're looking into it." |
| 502/503 | "Our servers are temporarily unavailable. Please try again in a few minutes." |
Implementation Patterns
A reusable error component pays for itself the first time a designer wants to change how errors look — you change one place instead of hunting through every form in the app. Here's the shape I'd reach for:
// Good error message component structure
const ErrorMessage = ({
title, // What happened
description, // Why (optional)
action, // How to fix
helpLink, // Where to get help
errorCode // For support reference
}) => (
<div role="alert" className="error-container">
<Icon name="alert-circle" />
<div>
<h4>{title}</h4>
{description && <p>{description}</p>}
{action && <p className="action">{action}</p>}
{helpLink && <a href={helpLink}>Get help</a>}
{errorCode && <small>Error code: {errorCode}</small>}
</div>
</div>
);Behind that component, the cleanest pattern splits responsibility across layers: the API returns structured error objects (something like { code: "INVALID_EMAIL", field: "email", message: "..." }) rather than raw strings, the frontend maps those codes to user-facing, localized copy, and logging captures the technical detail — error ID, stack trace, user context — server-side where it belongs. This separation is also what makes internationalization tractable: the backend deals in codes, the frontend deals in translated strings, and you're never trying to localize a message that was hardcoded three layers down.
Special Cases
Security-Sensitive Errors
| Scenario | ❌ Too Specific | ✅ Appropriately Vague |
|---|---|---|
| Login failure | "Password incorrect for user@email.com" | "Email or password is incorrect" |
| Account lookup | "No account exists with this email" | "If an account exists, we'll send a reset link" |
| Admin access | "You are not an admin" | "You don't have permission for this action" |
Why? Specific errors can help attackers enumerate valid accounts or understand system structure.
Errors with Sensitive Data
| Don't Expose | Instead Show |
|---|---|
| Database connection strings | "Database temporarily unavailable" |
| File paths on server | "File couldn't be processed" |
| API keys in error dumps | "Service configuration error" |
| User IDs of others | "Resource not found" |
For a full outage, the same principles scale up: link to a status page, give an honest estimate of resolution time if you have one, offer an alternative contact for urgent cases, and provide an incident ID so a support conversation doesn't start from zero.
What I Check Before Shipping an Error Message
I've settled into a short mental checklist rather than a long one, because a long checklist doesn't survive contact with a deadline. Clarity: does it say specifically what happened, in plain language, without leaning on a code the user can't interpret? Helpfulness: does it tell them what to do, and does it leave a way out if they can't do it themselves? Tone: does it avoid blaming them, avoid sounding alarmed, and acknowledge that this is genuinely inconvenient? And two categories that are easy to forget under deadline: accessibility (role="alert" so screen readers announce it, sufficient contrast, not relying on color alone) and security (never exposing system internals, never confirming or denying account existence in a way that helps an attacker enumerate accounts, sanitizing anything user-supplied before it's echoed back).
Measuring Whether It's Working
The metrics I'd actually watch: what fraction of errors turn into support tickets, what fraction of users successfully retry and recover on their own, and how long an error blocks someone before they either succeed or give up. If you have the traffic to A/B test, testing specificity ("Invalid input" vs. "Email must include @") and tone against retry-success rate tends to produce the most convincing internal argument for investing more time here — a concrete before/after retry-rate number moves a roadmap discussion faster than a style argument does.
Error messages are one of the few places where your product talks directly to someone who is already stuck, maybe already frustrated, possibly worried about lost work. "You failed, figure it out" and "here's what happened and here's how to fix it" are the same underlying event, described two different ways — and which one you write is the difference between a user who quietly gives up on your product and one who barely notices the error happened at all.