---
title: 'Error Message Writing'
source: 'https://academia.sh/en/courses/accessible-patterns/error-message-writing'
course: 'Accessible Component Patterns'
language: en
updated: '2026-08-19T05:19:52+00:00'
license: 'CC BY-SA 4.0'
---

# Error Message Writing

Classifying error messages by their source, each class's required parts, catching messages whose parts are complete but empty of content with a language audit, and deriving a message's placement and lifetime from its class.

The previous lesson's audit found its heaviest finding in an error message: "ISBN
entered wrong!" Its counter-example sat in the same catalog, and the difference was not
style but **structure**. The first says what was wrong; the second says what is right
and what the user should do.

This lesson treats an error message not as a writing problem but as a **structure with
defined parts**. The parts depend on the message's source, and until the source is
classified, no one can say which part is required.

## Errors Are Classified by Source

Five classes cover every error in the catalog interface. An **input error** is a value
the user typed that fails a format or range rule; the user can fix it. An
**authorization error** is an action outside the user's rights; the user cannot fix it
but can follow a path. A **conflict** is an action blocked by some other state — the
volume is already with someone else. A **transient error** is the system failing to
respond at that moment. An **unrecoverable error** is one where nothing the user does
changes the outcome.

The class determines three things: which parts are required, where the message is
placed, and how long it stays. The computation below runs these rules against six
messages gathered from the catalog.

```js
// error.mjs — classifying error messages by source and auditing their required parts

// Required parts per class. "solution" is a step the user can take.
const CLASS = {
  input:         { parts: ["reason", "solution"],          fieldScoped: true,  persistent: false },
  authorization: { parts: ["reason", "solution"],          fieldScoped: false, persistent: true  },
  conflict:      { parts: ["reason", "solution", "status"], fieldScoped: false, persistent: true  },
  transient:     { parts: ["reason", "solution"],          fieldScoped: false, persistent: false },
  unrecoverable: { parts: ["reason", "id"],                fieldScoped: false, persistent: true  },
};

// Messages gathered from the catalog interface.
const MESSAGES = [
  { id: "H1", class: "input", field: "isbn", text: "ISBN entered wrong!",
    parts: ["reason"] },
  { id: "H2", class: "input", field: "isbn",
    text: "The ISBN must be 13 digits; it appears under the barcode on the book's back cover.",
    parts: ["reason", "solution"] },
  { id: "H3", class: "authorization", field: null,
    text: "You lack permission to borrow this record. Borrowing rights need a student card; ask at the library entrance desk.",
    parts: ["reason", "solution"] },
  { id: "H4", class: "conflict", field: null,
    text: "This copy was borrowed before you got to it. It is due back August 12; you can join the reservation list.",
    parts: ["reason", "solution", "status"] },
  { id: "H5", class: "transient", field: null, text: "An error occurred. Please try again.",
    parts: ["reason", "solution"] },
  { id: "H6", class: "unrecoverable", field: null,
    text: "The loan record could not be created. You can contact the desk with reference number REC-4471.",
    parts: ["reason", "id"] },
];

// 1. Part audit: does each message carry its class's required parts?
console.log("message  class          required parts          missing");
let missingTotal = 0;
for (const m of MESSAGES) {
  const required = CLASS[m.class].parts;
  const missing = required.filter((p) => !m.parts.includes(p));
  missingTotal += missing.length;
  console.log(
    `${m.id.padEnd(8)} ${m.class.padEnd(14)} ${required.join(", ").padEnd(23)} ${missing.length ? missing.join(", ") : "-"}`
  );
}
console.log(`missing parts total: ${missingTotal}`);

// 2. Language audit: blaming pattern, exclamation mark, apology, and technical leakage.
const RULES = [
  { name: "blaming pattern", test: (m) => /\b(you entered|entered wrong|entered incorrectly|you forgot|you must)\b/i.test(m) },
  { name: "exclamation mark", test: (m) => m.includes("!") },
  { name: "apology pattern", test: (m) => /(sorry|apologize|unfortunately)/i.test(m) },
  { name: "empty courtesy", test: (m) => /(please try again|an error occurred)/i.test(m) },
  { name: "technical leakage", test: (m) => /(null|undefined|exception|error code \d|HTTP \d{3})/i.test(m) },
];
console.log("\nmessage  findings");
let findingsTotal = 0;
for (const m of MESSAGES) {
  const findings = RULES.filter((r) => r.test(m.text)).map((r) => r.name);
  findingsTotal += findings.length;
  console.log(`${m.id.padEnd(8)} ${findings.length ? findings.join(", ") : "-"}`);
}
console.log(`language findings total: ${findingsTotal}`);

// 3. Presentation decision: where does the message go, and how long does it stay?
console.log("\nmessage  placement                  persistence");
for (const m of MESSAGES) {
  const c = CLASS[m.class];
  console.log(
    `${m.id.padEnd(8)} ${(c.fieldScoped ? "next to the field" : "next to the action / page top").padEnd(28)} ` +
      (c.persistent ? "until the user dismisses it" : "until the next attempt")
  );
}

// 4. Message length: a readability ceiling.
const LIMIT = 120;
console.log("\nmessage  characters  over limit");
for (const m of MESSAGES) {
  console.log(`${m.id.padEnd(8)} ${String(m.text.length).padStart(10)}  ${m.text.length > LIMIT ? "yes" : "no"}`);
}

// 5. Roll-up: which messages need to be rewritten?
const rewrite = MESSAGES.filter((m) => {
  const missing = CLASS[m.class].parts.filter((p) => !m.parts.includes(p)).length;
  const findings = RULES.filter((r) => r.test(m.text)).length;
  return missing > 0 || findings > 0;
});
console.log(`\nmessages needing a rewrite: ${rewrite.length} / ${MESSAGES.length}`);
for (const m of rewrite) console.log(`  ${m.id}: ${JSON.stringify(m.text)}`);
```

```
message  class          required parts          missing
H1       input          reason, solution        solution
H2       input          reason, solution        -
H3       authorization  reason, solution        -
H4       conflict       reason, solution, status -
H5       transient      reason, solution        -
H6       unrecoverable  reason, id              -
missing parts total: 1

message  findings
H1       blaming pattern, exclamation mark
H2       -
H3       -
H4       -
H5       empty courtesy
H6       -
language findings total: 3

message  placement                  persistence
H1       next to the field            until the next attempt
H2       next to the field            until the next attempt
H3       next to the action / page top until the user dismisses it
H4       next to the action / page top until the user dismisses it
H5       next to the action / page top until the next attempt
H6       next to the action / page top until the user dismisses it

message  characters  over limit
H1               19  no
H2               82  no
H3              114  no
H4              105  no
H5               36  no
H6               94  no

messages needing a rewrite: 2 / 6
  H1: "ISBN entered wrong!"
  H5: "An error occurred. Please try again."
```

## Required Parts

The output's first section is the part audit. Every class requires a **reason**: the
user must know what happened. Input, authorization, conflict, and transient errors also
require a **solution** — a step the user can take. Conflict additionally needs a
**status**: when the copy comes back, or what the alternative is.

The unrecoverable error is the exception to this pattern. Offering a solution would be
misleading; instead an **id** is required, so the user has a reference number if they
end up talking to staff. This distinction was established in the Error and Warning
States lesson; here it is applied to the message text itself.

Only one message falls short of the part audit: "ISBN entered wrong!" gives the reason
but not the solution. Its counter-example gives both the rule and where it is written —
the digit count and the location under the barcode.

## Parts Complete, Message Empty

The second section is a language audit, and it catches a subtler defect. The transient
error message passes the part audit: it has a reason, and it has a solution. But neither
carries content. "An error occurred" is not a reason, and "try again" is an
unconditional suggestion — the user does not know how long to wait or why trying again
would help.

The audit flags this as an empty courtesy pattern. The same section also catches the
blaming pattern and the exclamation mark: "entered wrong" pins the fault on the user, and
the exclamation mark raises the volume. The technical-leakage rule belongs here too;
showing the system's internal error code gives the user no information, only eroding
trust. A reference number is not leakage: it is an identifier generated for this purpose,
one the user can hand to staff.

## Placement and Lifetime

The third section derives the presentation decision from the class. An input error sits
next to its field — which field is the problem can only be understood from position,
and this link is built into the markup the same way the Accessible Error Presentation
lesson describes. The other classes attach to the action or the page.

Lifetime also comes from the class. Input and transient errors clear on the next
attempt; the message loses its meaning once the user retries. Authorization, conflict,
and unrecoverable errors stay until the user dismisses them: retrying does not resolve
them, and the user may want to read the message, note it down, or pass it to someone
else.

The fourth section measures length. The limit is not a hard rule but a warning
threshold: once an error message reaches paragraph length, it is usually carrying two
pieces of information, and the second belongs in help text.

## What the Audit Produces

The last section flags two messages for a rewrite. What matters is not the count but
**which rule** fired: one is missing a part, the other has a part with no content. The
two defects need different fixes — one needs information added, the other needs its
placeholder content replaced with real content.

This audit is part of the guide file and runs under version control: a new message gets
its class assigned, its parts checked, and its language rules run. As the rule set
grows, it also produces false alarms, and each one is a record for narrowing the rule.

## Summary

- An error message is not a style problem; it is a structure whose parts are determined
  by its source. Reason is required in every class, solution in four classes, and in the
  unrecoverable class an id takes solution's place.
- The part audit catches missing information, the language audit catches empty
  information: "an error occurred, try again" passes the part audit but trips the empty
  courtesy pattern.
- Blaming suffixes, exclamation marks, apology patterns, and technical leakage are all
  auditable rules; a reference number is not leakage — it is an id the user can use.
- A message's placement and lifetime come from its class: input errors sit next to the
  field and clear on the next attempt; authorization and conflict errors stay until the
  user dismisses them.
- The audit says which rule fired; a missing part and an empty part call for different
  fixes.

## Next Step

These two lessons treated interface text in a single language. Once the catalog opens
into another language, the text's **surroundings** change along with the text itself:
the same message runs about thirty percent longer in some languages and stops fitting
its box, date and number formats change, and plural rules need more than two forms. The
next lesson takes on localization requirements and measures all of it: how text growth
affects layout, how formatting is derived from the locale, and why concatenating strings
breaks translation.
