Write error messages that say what to do next
Rewrites a vague error or validation message so it names exactly what went wrong, which rule failed and what to do next, without blaming the person.
Free, to use and to change. What it can see: only the chat you add it to.
Its short name: error-messages. Source on GitHub.
Who it helps
Install
Pick the one you use.
Claude Code
/plugin marketplace add mbsi-labs/skills
/plugin install error-messages@skills
Changelog
- 2026-10-21 (c6c2f30): Updated.
Licence: MIT.
Read the skill itself
When something fails, the person wants three things, in order: what went wrong, why (which rule), and what to do next. A good message answers all three in one breath and never blames them. A bad one only says that something failed.
The test: read the message as if you just hit the error and know nothing about the code. Can you tell exactly what to change next? If not, it's too vague.
Never ship a generic failure string: no "too weak", "invalid input", "save failed", "something went wrong", and never a raw code like import_failed or NotAuthorizedException. Be as specific as the call site allows.
The formula
[what failed] - [the specific reason] - [the concrete next step]
- What: the thing, in the person's words ("password", "this CSV file", "checkout"), not the function or exception name.
- Why: the specific rule or cause that fired, not the whole rulebook. "needs an uppercase letter", not "8+ characters, mixed case, a number".
- Next step: something they can actually do: a format to use, a field to change, something to remove, "check your connection and try again".
- Blame-free: describe the input, not the person. "needs an uppercase letter", not "you forgot a capital".
Not every message needs all three as separate phrases, but it must answer all three questions as far as the code knows the answer.
Examples
Validation: name the rule that failed, not all of them
// ❌ same message no matter which rule failed
return "password too weak (8+ chars, mixed case, digit)";
// ✅ check each rule and report only what's missing, client-side, before the server round-trip
function passwordProblem(pw: string): string | null {
const missing: string[] = [];
if (!/[a-z]/.test(pw)) missing.push("a lowercase letter");
if (!/[A-Z]/.test(pw)) missing.push("an uppercase letter");
if (!/[0-9]/.test(pw)) missing.push("a number");
const tooShort = pw.length < 8;
if (!tooShort && missing.length === 0) return null;
if (missing.length === 0) return "Password must be at least 8 characters";
const list = new Intl.ListFormat("en", { type: "conjunction" }).format(missing); // "a, b, and c"
const needs = `Password needs ${list}`;
return tooShort ? `${needs}, and must be at least 8 characters` : needs;
}
When a constraint has parts (length, case, format, size), check each part and name only the failing ones. Keep client-side validators in sync with the server rules they mirror.
Limits: say how to make room
❌ Storage full
✅ Storage full - delete old exports or upgrade to add space
Imports and uploads: say what's supported
❌ Couldn't import that file
✅ Couldn't read that file - upload a .csv or .xlsx with a header row
❌ Unsupported file type
✅ That's a .pages file - export it as .docx or PDF and try again
Never show a raw code
// ❌ setError(err instanceof Error ? err.message : "import_failed");
// ✅ setError(humanError(err) ?? "Couldn't import that file - check it's a CSV and try again");
Map error codes and exception classes to human text in one place (a humanError() function). A snake_case code or an ...Exception name must never reach the screen.
Keep developer hints out of production
return isDev
? "Auth service unreachable - is the local stack running? (docker compose up -d)"
: "Can't reach the sign-in service - check your connection and try again in a moment";
Deliberate exceptions
Specificity has two principled limits. Use them on purpose, and comment them so nobody "fixes" them later.
- Security. Sign-in stays generic: "Wrong email or password", never "No account with that email". Revealing which part was right lets attackers find valid accounts. The same goes for password reset ("If that email has an account, we've sent a link").
- Genuinely unknown cause. When a catch-all can't diagnose the failure, be specific about the action and still give a next step: "Couldn't save your changes - check your connection and try again", not "Save failed".
Placement and timing (when you control the UI)
- Show the message next to the field, not only in a summary at the top, and keep it visible while they fix it.
- Validate client-side before the server round-trip when you can.
- Validate on blur or submit, not on every keystroke.
- Don't rely on color alone. Pair it with text, and use
role="alert",aria-invalidandaria-describedbyso screen readers announce it.
These recommendations come from NN/g's guidelines for form errors and accessible validation practice.
CLIs and APIs
The same formula applies outside a UI:
❌ error: invalid argument
✅ error: --since expects a date like 2026-10-01, got "last week"
For APIs, return a stable machine-readable code and a human message that follows the formula. The UI shows the message; the client branches on the code.
Checklist
- [ ] Names the specific thing and the specific reason, not the whole rulebook
- [ ] Gives a concrete next step
- [ ] Describes the input, not the person
- [ ] No raw codes, exception names or stack details shown to people
- [ ] No developer-only hints in production text
- [ ] Security-sensitive messages kept generic on purpose, with a comment saying why
- [ ] Matches the surrounding product's voice and length