Error Messages That Help: Writing Copy People Can Act On
Photo: Mary Mark Ockerbloom, CC BY-SA 4.0, via Wikimedia Commons
An error message is a tiny contract between your software and the person using it. Most of the time it is invisible. When something goes wrong, it becomes the whole product. If it says 'invalid input' and nothing else, the user is left to guess whether they mistyped a postcode, hit a system limit or stumbled into a bug. If it says 'something went wrong' during a payment, they are left wondering whether they have been charged twice.
Good error text is not decorative. It is part of the delivery process. It should tell people what happened, what they can do next and whether the problem is theirs to fix or yours. That sounds obvious. It is also surprisingly rare.
The three jobs of an error
A useful error message does three things:
- Says what happened in plain English. No jargon, no internal codes as the main message, no blaming the user for failing to understand your data model.
- Says what to do next. Retry, correct a field, contact support, wait five minutes, check a setting. If there is no action, say so honestly.
- Preserves trust. A system failure should sound like a system failure, not like the user did something wrong. A user mistake should be specific enough to fix without a support ticket.
The distinction between user mistakes and system failures matters. 'Your postcode is not valid' is a user mistake. 'We could not save your changes because our database is unavailable' is a system failure. Both need plain English, but only one should ask the user to change their behaviour.
Web forms: be specific, not stern
Form errors are where most teams get their first chance to annoy someone. The usual pattern is a red border and a message that reads like a parking ticket. Better copy is specific, calm and close to the field.
Before: Error: Invalid input.
After: Enter a UK phone number, for example 07700 900123.
Before: Password does not meet requirements.
After: Your password needs at least 12 characters. It can include spaces.
Before: An error occurred.
After: We could not send the reset email. Check the address and try again.
The GOV.UK Design System has a useful pattern here: put a summary at the top of the page, link each message to the field and keep the language direct. That is not just a public-sector habit. It is good product design. A user should not have to decode 'validation failure' to work out that their date of birth needs to be in the past.
APIs: errors for developers are still user errors
Developers are users too. An API error that says { "error": "Invalid request" } forces every integrating team to guess. A structured error is kinder and cheaper to support.
Before: { "error": "Invalid request" }
After: { "code": "invalid_postcode", "message": "Enter a valid UK postcode, for example SW1A 1AA.", "field": "postcode", "docs_url": "https://example.com/docs/errors#invalid_postcode" }
The second version gives the developer a stable code for handling, a human-readable message for logs and a field name for debugging. It also avoids the classic mistake of returning a 200 response with an error inside it. If something failed, say so with the right status code. If the client can fix it, explain how. If they cannot, say what your team is doing about it.
CLI tools: errors at the terminal
Command-line tools have a reputation for terse errors, which is fair. A missing file should not produce a single code and nothing else.
Before: Error: ENOENT
After:
cc build failed: could not find config file at ./codecrafting.config.js
Create one with: cc init
Run with --verbose for details.
The second version says what failed, where the tool looked and what to try next. It also gives the user a way to get more detail without opening a search engine. That is the whole job. Exit codes still matter for scripts, but they are not a replacement for a sentence.
A code review checklist for error text
Error copy is easy to ignore in review because it often arrives as a string literal at the bottom of a diff. Treat it like any other user-facing behaviour and ask:
- Does it say what happened, using plain English?
- Does it tell the user what to do next?
- Is it clear whether this is a user mistake or a system failure?
- Does it avoid blame? Words like 'illegal', 'forbidden' and 'you failed' can go.
- Does it avoid internal jargon such as 'null', 'undefined', 'exception' or 'payload' unless the audience is technical and expects it?
- Does it include a stable error code for support and logs, kept separate from the human message?
- Does it avoid leaking sensitive information such as file paths, SQL fragments or stack traces?
- Is it consistent with UK spelling and local formats? 'Organisation', 'postcode' and DD/MM/YYYY are good clues.
- Is it tested? At minimum, check the message appears in the right place and does not break the layout.
That checklist is not about being precious. It is about treating failure as part of the product. A clean build is not just one that compiles. It is one that fails in a way a person can understand.