Writing Error Messages With LLMs in November 2026: Give the Model the Real Failure, Say What Happened and What to Do Next, and Keep Secrets Out of the Text
An error message is the one piece of text your users read when they are already annoyed. "Something went wrong" tells them nothing. "Error 0x80004005" tells them less. Language models are good at turning a stack of technical facts into a calm, readable sentence, so it is tempting to hand the whole job over. That works, but only if you give the model the right inputs and check its output against a few hard rules. This guide covers how to write error messages with an LLM's help for both end users and developers, without ending up with friendly text that is wrong, vague, or leaks things it should not.
What a Good Error Message Has to Do
Before you prompt anything, agree on what "good" means. A useful error message answers three questions, in this order:
- What happened? In plain words, from the reader's point of view. "We couldn't save your changes" rather than "PUT /api/doc failed".
- Why, if you know? Only when the cause is actually known. "The file is larger than the 25 MB limit" is helpful. A guessed cause is worse than none.
- What can I do now? A concrete next step: retry, shorten the input, sign in again, contact support with a reference code.
Developer-facing errors (CLI tools, libraries, API responses) follow the same pattern but can include more detail: the field name, the value that was rejected, the expected format, and where to read more. The model's job is to fill in this structure, not to invent a new one each time.
A wall of technical text tells the person in front of the screen almost nothing about what to do next. Image: Blue Screen of Death on an arcade machine by Pablo Gómez, Public domain, via Wikimedia Commons.
Step 1: Inventory the Failures Before You Write Anything
The most common mistake is asking a model to "improve our error messages" with no list of what can actually fail. You get polished copy for errors that never happen and nothing for the ones that do. Start by collecting the real failure cases:
- Grep your code for places that raise, throw, or return error responses. In a typical web app that means exception classes, HTTP 4xx/5xx handlers, and validation schemas.
- Pull the most frequent error strings from your logs or error tracker for the last few weeks. Those are the messages people actually see.
- Read recent support tickets and note which errors confused people. A message that generates tickets is a message worth rewriting first.
Put each case in a simple table: an internal code or exception name, the condition that triggers it, whether the cause is known for certain, whether the user can fix it, and who reads it (end user, developer, or operator). This table is your real prompt input.
Step 2: Give the Model Facts, Not Vibes
A model writing an error message without context will happily guess. Give it the trigger condition, the limits involved, and the actions that genuinely exist in your product. A prompt that works well looks like this:
You are writing user-facing error messages for a document editor.
Audience: non-technical users in a web browser.
Style: plain English, no blame, no jargon, no exclamation marks,
at most two short sentences plus an optional action label.
For each case below, write:
- message: what happened and, only if "cause_known" is true, why
- action: one concrete next step that uses ONLY the listed actions
Do not invent limits, features, or support channels.
Available actions: "Try again", "Reduce file size",
"Sign in again", "Contact support (include reference code)"
Cases:
1. code=UPLOAD_TOO_LARGE, cause_known=true, limit=25 MB,
user_fixable=true
2. code=SESSION_EXPIRED, cause_known=true, user_fixable=true
3. code=SAVE_FAILED_UNKNOWN, cause_known=false, user_fixable=false
The "use only the listed actions" line matters. Without it, models routinely suggest "clear your browser cache" or "check our status page" when you have no status page. Every action the message mentions has to exist.
Step 3: Write Developer Errors That Point at the Problem
For APIs and CLIs, the reader can handle detail, and they usually need it. A good developer error names the input, says what was expected, and shows what was received. Compare:
Bad: Invalid request.
Good: Field "start_date" must be an ISO 8601 date (YYYY-MM-DD).
Received "11/09/2026". See /docs/errors#invalid-date.
When you ask a model to rewrite developer errors, paste the actual validation rule or schema, not a description of it. If the code accepts dates in two formats, the model can only say so if it sees both. Ask it to keep a stable machine-readable code alongside the human text, for example {"code": "invalid_date", "field": "start_date", "message": "..."}. Client code should branch on the code, never on the wording, so you can improve the wording later without breaking anyone.
Step 4: Keep Secrets and Internals Out of the Text
This is the rule a model will not enforce for you. If you paste a raw exception into the prompt and ask for a friendlier version, the model may carry details forward: a database host name, a file path on the server, a SQL fragment, a user's email address, or an API key that happened to be in the stack trace. Some practical safeguards:
- Redact before you prompt. Strip tokens, connection strings, and personal data from the examples you send to the model, the same way you would before pasting into any third-party tool.
- Separate what users see from what you log. The user gets the friendly message and a reference code. The full exception, stack trace, and request details go to your logs under that reference code.
- Do not echo raw input back in HTML without escaping it. "Received <script>..." is a classic way to turn an error page into a cross-site scripting hole.
- Be careful with messages that reveal whether an account exists. "No account with that email" on a login form tells an attacker which addresses are registered. "Email or password is incorrect" does not.
Ask the model to review its own drafts for these problems as a second pass, but treat that as a hint, not a guarantee. A short automated check that fails the build if a message template contains things like stack trace markers, internal hostnames, or variables you never meant to expose is more reliable.
A good warning light is specific and shows up where you are already looking. Error messages should do the same. Image: Dashboard display showing warning lights in a vehicle during nighttime driving by Shixart1985, CC BY 2.0, via Wikimedia Commons.
Step 5: Review the Output Like Copy, Then Like Code
Read every generated message out loud against its case. Look for these common problems:
- Invented causes. The case said the cause was unknown, but the message says "due to a network issue". Delete the guess.
- Fake precision. "Please wait 5 minutes" when your rate limit actually resets after 60 seconds. Every number must come from your code or config.
- Blame. "You entered an invalid date" reads worse than "That date isn't in a format we recognize". Small wording changes reduce frustration.
- Over-apologizing. One "sorry" is fine for a failure on your side. Three in a row sounds insincere and pushes the useful part further down.
- Same text for different problems. If two distinct failures produce identical messages, support cannot tell them apart. Give each its own code even if the wording is similar.
Then treat the messages as code. Store them in one place (a message catalog or translation file) instead of scattering strings through handlers. Write tests that trigger each error and assert the code and the presence of an action. If you translate your UI, keep placeholders like {limit} as variables so the number stays correct in every language.
Step 6: Check Them in the Real Interface
An error that reads well in a spreadsheet can still fail on screen. Trigger each one in a browser or terminal and check:
- It appears next to the thing that failed. A form field error belongs under that field, not in a toast that vanishes after three seconds.
- Screen readers announce it. For web forms that usually means associating the message with the input and using an appropriate live region, then testing with an actual screen reader.
- It fits. Long messages get truncated in mobile layouts and narrow terminal windows.
- The action works. If the button says "Try again", clicking it should retry the same operation, not reload a blank page.
A Short Before-and-After Example
Here is the kind of change this process produces for a common upload failure:
Before: Error 413: Request Entity Too Large
After: This file is too large to upload. Files can be up to 25 MB.
[Choose a smaller file]
And for a failure where you genuinely do not know the cause:
Before: Unhandled exception in SaveHandler
After: We couldn't save your changes. Your work is still in this tab,
so you can try again. If it keeps happening, contact support
and mention code R-7F3K.
Note what the second message does not do: it does not guess at a cause, and it only promises "your work is still in this tab" if that is actually true in your app. If unsaved changes would be lost on reload, say so instead.
A Workable Checklist
- List real failure cases from code, logs, and support tickets.
- Record for each one: trigger, whether the cause is known, whether the user can fix it, and the audience.
- Prompt with that table, a style guide, and the exact actions that exist.
- Keep a stable machine-readable code next to every human message.
- Redact before prompting, and keep internals in logs behind a reference code.
- Review for invented causes, fake numbers, blame, and duplicate wording.
- Store messages in one catalog and test that each error fires with its code.
- Trigger every message in the real interface, including with a screen reader.
The model is a fast copywriter here, not a source of truth. You supply the facts about what failed and what the user can do, and it turns them into sentences people can act on. Keep that division of labor and your error messages will get clearer without getting less accurate.
Comments
Post a Comment