Error messages that keep users moving do three things: they say what happened in plain words, they explain why only when that helps, and they tell the person exactly what to do next, right beside the thing that needs fixing. They don't blame, they don't joke, they don't lead with a raw code, and they never wipe what the user already typed. For a solo maker, the fastest win is an inventory: list every error your product can show, rewrite the ones behind the most support emails, then break your own app on purpose to find the rest. It's an afternoon or two of work.
Below: the three parts of a useful message, tone, placement and timing, form validation, system and permission errors, the inventory, a "break it" session, a made-up example, a checklist, traps and a 7-day plan.
Why are error messages product work, not polish?
Because users meet them at the worst possible moment. They were trying to do something, it didn't work, and now they're reading your words with less patience than they had a second ago.
A vague error at that moment usually ends one of two ways: the user emails you "it doesn't work," or the user quietly leaves and you never hear about it.
Nielsen Norman Group lists this as usability heuristic #9, "Help Users Recognize, Diagnose, and Recover from Errors", and its error-message guidelines from NN/g note that teams polish the happy path and treat everything off it as an afterthought. Google's technical writing course on helpful error messages puts the payoff plainly: good error messages let users help themselves and reduce the support workload.
When you're the only person answering support, that second part is your evenings.
What goes into a useful error message?
Google's course boils it down to two questions every message should answer: what went wrong, and how does the user fix it? I find it easier to write with three parts:
What happened. In the user's terms, not your code's. "Your file didn't upload," not "POST /api/assets failed."
Why, if it helps. Only include the cause when knowing it changes what the user does. "The file is 14 MB" helps. "An unexpected exception occurred" doesn't.
What to do next. One clear action. Try again, pick a smaller file, choose a different date, ask a named person for access.
A few error message examples, before and after:
Before: "Invalid input." After: "Project name can be up to 60 characters. This one has 74."
Before: "Upload failed." After: "This photo is 14 MB. Photos can be up to 10 MB. Try a smaller version or a screenshot."
Before: "Error 500." After: "We couldn't save your changes. Your text is still here. Try again in a moment, and if it keeps happening, send us error ID 7F3K-22."
The error ID stays, but at the end. It matters; it just shouldn't be the headline.
What tone should error messages use?
Good UX error messages sound plain, calm and specific. Write it the way you'd say it to someone sitting next to you. The GOV.UK Design System's guidance for the error message component even suggests reading the message out loud to check it sounds like something a person would say. It's a good test.
The rules I'd hold to:
No blame. NN/g advises against "invalid," "illegal" and "incorrect" because they imply the user did something wrong. GOV.UK adds "forbidden," "prohibited" and "you forgot."
No jokes on real failures. NN/g notes humour goes stale when people see the same error often. A pun is cute once; on the fourth failed save it's insulting.
No "Oops!" and no "Something went wrong" dead ends. GOV.UK lists "oops" as language to avoid, and generic lines like "An error occurred" give the user nothing to act on.
No raw codes or stack traces as the main text. NN/g says to show obscure codes only for diagnosis. Keep a labelled error ID at the end so the user can quote it to you.
Go easy on "sorry." GOV.UK drops "sorry" and "please" entirely for its services. You don't have to be that strict in a small SaaS, but one apology is plenty and it never replaces the fix.
Short, plain messages also tend to translate better, one more reason to cut the cleverness.
Where should an error message appear, and when?
As close as possible to whatever caused it (NN/g's first visibility rule), and not before the user has had a fair chance to finish.
Inline, next to the field: anything the user typed or picked. This is where most errors belong.
Summary at the top of the form: when a long form has several problems. GOV.UK says to use the same wording here and next to each field.
Toast or small banner: things that don't block the user. Never put a blocking error in a toast that vanishes after four seconds.
A full page: problems the user can't fix from the form. GOV.UK says not to use field errors for permission or capacity problems; use a page that explains what to do instead.
Timing matters as much as placement. NN/g calls showing errors too early "a hostile pattern" and compares it to grading a test before the student has had a chance to answer. Their example: flagging an empty email box just because someone clicked into it and moved on. A sensible default is to validate when the user leaves a field they typed in, or on submit, not on every keystroke. Live guidance while typing is only worth it for input that's genuinely hard to get right first time.
And keep their input. GOV.UK is explicit: don't clear form fields when showing an error, and keep both passing and failing answers so people can edit them. A form that wipes itself after one mistake is how you lose people for good.
One short note on visibility: red alone isn't enough. NN/g says never to rely only on colour or animation to signal an error, so pair the colour with text and an icon, and connect the message to its field in your markup so screen readers announce them together.
How do you write form validation messages?
The best validation message is the one that rarely shows up, because you stated the rule before the user typed anything. Put the rule in a hint under the label: "Up to 60 characters," "JPG or PNG, up to 10 MB," "For example, 24/10/2026." Then write a specific message for each way the field can fail.
GOV.UK makes this concrete: "This field is required" and "An error occurred" are too general, and different failures need different messages. A text field can be empty, too long, too short, in the wrong format, or using characters you don't accept. Each gets its own sentence.
Field and rule | Hint (shown up front) | Error (only when it fails) |
|---|---|---|
Project name, required | None needed if the label is clear | Enter a project name |
Project name, max 60 characters | Up to 60 characters | Project name must be 60 characters or fewer (this one has 74) |
Start date, must be today or later | For example, 24/10/2026 | Start date must be today or later |
File upload, size limit | JPG or PNG, up to 10 MB | This file is 14 MB. Choose a file up to 10 MB |
Three habits help. Reuse words from the label, as GOV.UK recommends, so the message clearly belongs to its field. Include the actual limit and, where you can, the actual value. And if you can guess the fix, offer it: NN/g suggests letting users pick a correction, like the city that matches the postcode they entered.
What should system and server errors say?
When the problem is on your side, the message has one job: protect what they were doing and give them a way forward.
Google's course warns against hiding the root cause behind a catch-all "Server error," because a dropped connection, a permission issue and a service failure all end up looking identical. Separate them where you can; each needs a different next step:
Timeouts: "This is taking longer than usual. Your work is saved as a draft. Try again in a minute."
Rate limits: say when they can try again. "You've sent a lot of invites in a short time. You can send more at 3:40 PM."
Upload failures: say which file failed and keep the ones that worked. Never make someone re-upload ten files because one broke.
Import failures: give row numbers and the reason per row, and import the good rows if your product can. "Rows 14 and 37 have no email address" beats "Import failed" by a mile.
Offline: say it's the connection, not their data, and keep the form filled in.
"Your session expired": keep it to a short line, and if you can, hold on to what they were editing until they're back.
For anything you can't predict, offer three things: a Try again button, a way to keep their work (a saved draft, or at least the text still on screen), and a contact link that pre-fills the error ID, the page and the time. That link turns "it's broken" into a message you can act on. (Logging on your side is a separate job; this post is about what the user reads.)
And don't fail silently. Google's course warns that silent failures leave users wondering whether anything happened. A button that does nothing is an error message with no words.
How do you handle permission and destructive errors?
"You don't have permission" is half a message. The user's next question is always "so who does?" Answer it: "Only workspace admins can invite new members. Ask Priyanka or another admin to give you access." If you can, add a button that sends that request for them.
Same for actions your product blocks to protect data. "Can't delete this item" leaves people stuck. "This template is used by 3 active projects. Move them to another template first, then delete it" tells them what to do. If an action really can't be undone, say so before they click, not after.
What should your app's 404 page say?
Keep it short. Say the page isn't here (maybe it moved, or the link was mistyped), and offer the places people most likely wanted: their dashboard, search, and a way to contact you. Skip the giant illustration and the joke; a stranded user wants a way out, not a mascot.
How do you find every error message you already ship?
Make an inventory. SaaS error messages live in more places than most makers remember. Search your codebase for where messages are created or shown. The pattern depends on your stack, but something like rg -n "throw new Error|setError|toast.error|message:" src/ plus a look through any translation or strings file gets you most of the way. Check your backend too: API error text often reaches users word for word.
Put every message in a sheet:
Where | Trigger | Current copy | Rewrite | Owner / status |
|---|---|---|---|---|
Project settings form | Name over 60 characters | Invalid input | Project name must be 60 characters or fewer (this one has 74) | Me / done |
Bulk import | Rows missing a required column | Import failed (422) | We imported 41 of 45 rows. Rows 9, 12, 30 and 44 have no email address | Me / this week |
Then sort by pain, not by order of discovery. Go through recent support emails and tag the ones caused by a confusing error; if you don't do this already, this walkthrough on turning support tickets into product priorities shows a simple way to tag them. Fix the top five first. If an error needs more than two sentences, link it to a short help article; this guide to writing five help docs that cut support load covers which ones are worth writing. GOV.UK also recommends tracking how often people see each error, a quick way to spot the field that trips everyone up.
How do you test your error messages on purpose?
Book 30 to 45 minutes and try to break your own product. This isn't a QA pass; you're only reading what users see when things go wrong.
Type wrong things into every field in your core flow: too long, empty, wrong format, emoji, a date in the past.
Turn on airplane mode halfway through a save, then turn it off again.
Upload a huge file and a file of the wrong type.
Import a CSV with missing columns and a few broken rows.
Leave a tab open overnight and try to save in the morning.
Use a second account without admin rights and try the admin actions.
Screenshot every message and add it to the sheet. Anything that wipes input, shows a raw code, or says nothing goes to the top.
A made-up example: Toolquay's error messages, before and after
Everything in this example is made up: the product, the maker and every number. Toolquay is a booking app for community tool libraries and makerspaces. Members reserve tools, and admins import inventory from a spreadsheet and add photos. Its maker, Kofi, noticed most of his support emails started with "I tried to..." and ended with a screenshot of an error.
He ran the inventory, found 38 messages, and rewrote the six that showed up most in support:
Situation | Before | After |
|---|---|---|
Pickup date in the past | Invalid date | Pickup date must be today or later (hint above the field: For example, 24/10/2026) |
Tool photo too big | Upload failed | This photo is 14 MB. Photos can be up to 10 MB. Try a smaller version or a screenshot |
Inventory import with bad rows | Import error (code 422) | We imported 212 of 230 tools. 18 rows have no tool name. Download those 18 rows, fix them, and import just that file |
Tool booked by someone else meanwhile | Error: conflict | Someone booked the tile saw for 24 Oct while you were choosing. It's free from 26 Oct. Pick another date |
Member tries to edit inventory | 403 Forbidden | Only space admins can edit the inventory. Ask Sam (admin) for access, or suggest a change |
Connection drops during booking | Something went wrong | Couldn't reach Toolquay. Check your connection and tap Try again. Your booking details are still filled in |
He also added a "Contact us about this" link under every system error, pre-filled with the error ID and page. In this made-up story, the next month brought fewer "it doesn't work" emails and more that quoted an error ID, which were faster to answer. The point isn't the numbers; every row above went from a dead end to a next step.
An error message rewrite checklist
Check | Pass if... |
|---|---|
What happened | It's described in the user's words, not your code's |
Next step | There's one clear action (retry, change X, ask a named person) |
Specific | It names the field, the limit and the actual value where relevant |
Tone | No blame words, no "Oops," no jokes on real failures |
Codes | Any error ID is last, labelled, and copyable |
Placement | Next to the field, or on a page if the user can't fix it |
Timing | Shown on leaving a field or on submit, not while typing |
Input kept | Nothing the user typed or uploaded is lost |
Not colour alone | Text and an icon accompany the red |
What traps make error messages worse?
Blaming the user. Describe the rule, not their failure.
Vague "invalid input." Invalid how? Say which rule it broke.
Jokes. Fine on a harmless empty search, grating on a failed save.
Codes only. "Error 0x0000000643" is for your logs, not your users.
Errors that clear the form. A small mistake becomes a lost user.
Red-only signalling. Some users won't see it. Add text and an icon.
One generic toast for everything. Nobody, including you, can tell the failures apart.
Errors before the user finished typing. Flagging an email after the first letter is the "grading the test early" problem.
A 7-day plan to fix your error messages
Day | Task | Time |
|---|---|---|
1 | Search the codebase and strings files; start the inventory sheet | 60-90 min |
2 | Tag the last month of support emails by the error they mention; rank the top five | 45 min |
3 | Rewrite the top five with the three-part pattern; run each through the checklist | 60 min |
4 | Add hints for the rules behind your most common validation errors; check forms keep input | 60 min |
5 | Add Try again, saved drafts or kept input, and a pre-filled contact link with the error ID to system errors | 90 min |
6 | Run a 30-45 minute "break it" session; add new finds to the sheet | 45 min |
7 | Ship the fixes, fix your 404 page, and set a monthly 20-minute review of the sheet | 45 min |
Those times are rough rules of thumb for a small product, not a benchmark.
FAQ
Should error messages say "sorry"?
Sometimes, once. If your system failed, a short apology is human. But GOV.UK's point stands: "sorry" doesn't fix anything, so it never replaces the next step. For validation errors, skip it.
Should I show error codes at all?
Yes, as a secondary detail. A short, copyable error ID at the end of a system error helps you find the exact failure when someone writes in. Never the headline, never a stack trace.
Who should write error messages in a one-person company?
You, but not in the middle of coding the feature. Leave a placeholder, then do a copy pass with the checklist once the flow works.
Make the bad moment short
Users will hit errors; what you control is how long the bad moment lasts. That's most of how to write error messages: say what happened, say what to do, keep their work, and put the message where their eyes already are. Run the inventory this week and fix the top five. If you want real people to try your flows and tell you where they got stuck, list your product on MakerHunt and ask other makers for feedback on the parts that break.