Skills

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

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]

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.

Placement and timing (when you control the UI)

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