Lesson 22 / 25
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.
Contents
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.
// 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.
To keep your progress and take notes, Log in
My notes
Log in to take notes.