# 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](https://github.com/mbsi-labs/skills/tree/main/plugins/error-messages/skills/error-messages).

## Who it helps

> **Helps:** "Our failures say 'invalid' or 'something went wrong'."

> **Does not help:** Marketing copy or long help pages: it only writes the failure line.

## 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.

## 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

```ts
// ❌ 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

```ts
// ❌ 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

```ts
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-invalid` and `aria-describedby` so screen readers announce it.

These recommendations come from [NN/g's guidelines for form errors](https://www.nngroup.com/articles/errors-forms-design-guidelines/) and [accessible validation practice](https://www.uxpin.com/studio/blog/accessible-form-validation-best-practices/).

### 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

---

Published 2026-10-21. Web page: https://www.mackenziebowes.com/skills/error-messages Part of [Skills](https://www.mackenziebowes.com/skills).
