Development
Key points

Key points

  1. 1Every API or CLI error needs four parts: what failed, the input or resource it failed on, why in plain words, and the exact next step.
  2. 2Put the fix in the message body as a concrete value, flag or command; add one link to the docs section for that error code, not the docs home page.
  3. 3A machine-readable error code and a request ID do different jobs: the code drives programmatic handling, the request ID makes support traces possible.
  4. 4Test each message by handing it to someone who did not write the code and asking them to recover without asking you.

The four-part error message

Most error strings are written at the moment the exception is thrown, by the person closest to the bug and furthest from the user. That is why they read like Error: invalid input. It tells you a check fired, but nothing about which check, which value, or what to do next.

The pattern that fixes this asks for four things, in this order:

  1. What failed. The operation, stated as the user asked for it: "Could not create the billing report," not "ValidationException in ReportFactory."
  2. On which input or resource. Name the field, parameter, file, endpoint or resource ID, and show the value that failed when it is safe to echo.
  3. Why, in plain words. The constraint that was violated, not the name of the internal constant that enforces it.
  4. The exact next step. A corrected value, a flag to add, a command to run, or a state to change. Something the reader can act on without another round trip.

The API design guidance behind this is consistent: errors should be as detailed as possible, provide context as to exactly what went wrong and why, help humans find more information, and help computers decide what to do next, all in a consistent shape across the API (Speakeasy). Postman's error handling guidance adds that the consumer should be able to understand the problem and how to fix it from the message alone (Postman).

Here is the same failure written three ways. Each one passes one more test.

# Level 0: tells you nothing
Error: invalid request

# Level 1: names the field
Error: "limit" is invalid

# Level 2: names the field, the value, the rule and the fix
Error: "limit" must be between 1 and 100 (you sent 0).
Use limit=50 or omit it to use the default of 50.

Level 2 costs one more sentence to write and removes a support thread. That trade is almost always worth it, because the person reading your error is often a stranger to your codebase, and their next action is either to fix the call or to give up.

A useful discipline: write the message so it survives being read alone in a CI log six months from now, with no surrounding context. If it needs the stack trace above it to make sense, it is not finished.

Getting the wording right: specific, human, never vague

The single-word failures to ban are invalid, bad, failed, malformed and unexpected. Each one describes the system's judgment rather than the world. "Invalid" says the input broke a rule; it does not say which rule.

Nielsen Norman Group's error message guidelines put the design goal plainly: help users recognize, diagnose and recover from errors (NN/g). For a developer audience the same three verbs map cleanly:

  • Recognize: the error code and the first line make the category obvious at a glance.
  • Diagnose: the message names the input and the constraint, with the offending value echoed.
  • Recover: the message contains the fix, or a link that lands within one screen of it.

Compare the vague version with the specific one for a CLI that validates a configuration file:

# Vague
Error: config invalid

# Specific
Error: config field "retries" must be a whole number, got "three"
  file: ./pipeline.toml, line 14
  fix: set retries to an integer such as 3
  docs: https://example.com/docs/config#retries

The specific version names the field, the type it expected, the value it received, the file and line, the corrected shape, and a link. A reader can open the file, change one line, and rerun. The vague version forces them to guess which of a dozen settings is wrong.

One caution: never echo secrets, tokens or full request bodies. Postman's guidance is explicit that error messages should only include information about the problem and possible fixes, never sensitive data (Postman). Echo the field name, the constraint and a safe excerpt, such as a length or a masked value, not the raw payload.

Command line tools have an extra advantage worth using: they can suggest the right flag. AudioCodes' CLI reference shows a good small habit, where an out-of-range value reports the valid range alongside the rejection (AudioCodes). That is the smallest possible fix statement: the value you sent, the range allowed. Every numeric or enumerated parameter in your interface can do the same.

A field-by-field template for the error body

A consistent envelope matters as much as good prose, because consistency is what lets client code branch on the response and lets support staff read it. The table below is a working template you can adapt. It separates what humans read from what programs read.

FieldAudienceJobExample
HTTP statusBothBroad category: 4xx client, 5xx server400
codeMachineStable identifier for programmatic branchinglimit_out_of_range
messageHumanWhat failed, on what, and why, in one or two sentences"limit" must be between 1 and 100 (you sent 0). Use limit=50 or omit it for the default.
paramBothThe exact field or resource, so tooling can highlight itlimit
doc_urlHumanOne link to the section for this codehttps://example.com/docs/errors#limit_out_of_range
request_idSupportCorrelates the response with server logsreq_8f21c4a0
retry_afterMachineSeconds to wait before a safe retry, on 429 and 50330

Two rows carry more weight than their size suggests.

The code is the contract. Speakeasy's write-up points out that status codes only set the scene, and that an application code lets clients handle the case programmatically, so that if (error.code === 'trip_too_short') is possible instead of string matching on prose (Speakeasy). Treat codes as append-only: adding is fine, renaming breaks every client that branched on the old string.

The request_id is what turns a support ticket into a five-minute lookup. Postman notes that returning a request ID and timestamp with the error helps teams trace interactions and debug across multiple calls (Postman). Make it visible in the message or, for CLIs, print it in the output so it reaches the ticket unedited.

Keep the envelope identical for every error. Mixed shapes (sometimes a bare string, sometimes a nested object) force every client to write defensive parsing, and the cost of that lands on your users, not you.

Documentation frameworks give a clean answer to where an error link should land. Diátaxis separates documentation into four kinds that serve four different needs: tutorials, how-to guides, reference and explanation (Diátaxis). Using that split, the question of which page an error should link to mostly answers itself.

A single error message is about a fact and a task at once. The fact is "this parameter has these constraints," which belongs in reference, the kind of documentation that describes what the thing is with accuracy and completeness (Diátaxis start here). The task is "change your call so it succeeds," which is how-to territory.

The practical decision rule is this: link to the specific reference anchor for the parameter or code, on a page that also contains a short correction example. If your docs do not yet have per-code sections, create them, and make the anchor stable so error output can point at it forever. Never link to the docs home page, and never link to a search query. Both add a navigation step exactly when the reader is already blocked.

A second rule: one link per error. More than one forces a choice. The link should be close to the fix in the message, on the line after it, so that reading order does not break the flow.

If your team publishes docs from source, docs-as-code workflows let you keep the error code list and the reference page in the same repository and the same review, so a new code cannot ship without its page. The practice applies the same version control, pull requests and review to documentation as to code, and allows you to block a merge when the docs are missing (Write the Docs). The UK Government Digital Service describes the same benefit for keeping docs in sync with products that change often (GOV.UK Technology Blog).

A small workflow that makes this concrete, since error messages are code:

# Fail the build if any error code in the source lacks a docs anchor
codes=$(grep -rhoE 'code: "[a-z_]+"' src/ | cut -d'"' -f2 | sort -u)
for c in $codes; do
  grep -q "id=\"$c\"" docs/errors.md || echo "missing docs: $c"
done

If that prints nothing, every code your service can return has a page someone can land on. If it prints a list, you have found the support tickets before they were written.

Error codes, categories and the retry decision

A good code does more than label a failure. It carries the decision the caller has to make. For most APIs that decision reduces to three options: fix the request, wait and retry, or escalate. Encode that in how you group and name codes, and both humans and programs get faster.

Error situationStatusCaller actionWhat the message must contain
Bad parameter, fixable400Fix and resendThe field, the rule, the corrected value
Not authenticated401Refresh or re-send credentialsWhich credential is missing or expired
Not authorized for this resource403Stop, contact the ownerThe scope required, not just "denied"
Missing resource404Check the IDThe ID as received, without leaking whether it exists elsewhere
Conflict with current state409Re-read, then retryThe current state and what to change
Rate limited429Wait, then retrySeconds to wait (retry_after)
Server fault500, 503Retry with backoff, then escalateRequest ID and a link to status

The server-side pattern is to document every code with its remediation, not just the happy path. Postman's guidance says the API documentation should include possible error codes, common messages and remediation suggestions, and that the error structure should be uniform across requests (Postman). Note how the 403 row differs from 429: one says stop and escalate, the other says wait thirty seconds. If both read "Forbidden" and "Too many requests," the caller has to know your internals to tell them apart, and they will guess wrong.

On the CLI side, the same logic applies with a different surface. A CLI error can print a corrected command, which is the most actionable error a developer will ever read: the exact line to paste. When a tool can infer the fix (a missing flag, an out-of-range value, a typo close to a real subcommand), printing the corrected invocation converts a failed run into a successful one with one copy and paste.

The public documentation for APIs points the same way. Stoplight's guide notes that good API documentation covers reference, guides and examples, and that the best docs help a developer complete an integration rather than understand the product's internals (Stoplight). Error pages sit in all three categories at once, which is why per-code sections with an example are worth the effort: they are reference, how-to and example in one anchor.

Testing messages with people who did not write them

The only real test of an error message is whether a stranger can act on it. Reading it aloud in a review catches typos; it does not catch missing information, because reviewers already know the answer.

A usability-style test for error text works well and is cheap. Nielsen Norman Group's guidelines frame effective errors around visibility, constructive communication and respect for user effort (NN/g). Translate those into three questions you ask the tester after they hit the failure:

  1. What happened? If they cannot say which operation failed and on what input, the "what" and "which" parts are missing.
  2. What will you do next? If the answer is "look at the docs" or "ask someone," the fix line is not doing its job.
  3. How long did the recovery take? Time to a corrected request is the metric that matters. A message that halves it has paid for itself.

Run the test with the person at a terminal or a code editor, given the error and nothing else. Do not hand over the request that caused it. Do not answer questions. If they recover without you, pass. If they stop to search, note the words they searched for, then put those words into the message or the anchor page.

Three habits make this practical at team scale:

  • Write the message before the code that throws it. The check exists to prevent something, so the fix statement is knowable at design time. Writing it first tends to simplify the validation too.
  • Keep a golden file of error strings per code. A test that asserts the message contains the field name, the rule and the fix line catches the regression where someone refactors the check and drops the helpful part.
  • Review every new error code against the four parts. What failed, which input, why, what next, and a link. Five checkboxes, thirty seconds in code review.

If your team generates developer-facing content, a coding agent can help draft the first version of an anchor page from each error code and its test, leaving the review to a human who knows the constraint. The skill library at Quanta Skills is built for that kind of install-and-run task inside an agent. For teams building or documenting a whole developer portal around these errors, Quanta takes a described site or document and builds it with you.

A short checklist before you merge

The pattern compresses into a checklist you can put in a pull request template. It is deliberately short, because a long checklist gets skipped.

  • The message names the operation, the field or resource, the value or state received, the rule broken, and the fix.
  • No sentence contains invalid, bad, malformed or unexpected on its own.
  • The response carries a stable code alongside the prose message.
  • The doc_url points to the anchor for that code, and the anchor exists. A broken error link is worse than none.
  • A request_id is present and printed in CLI output.
  • Secrets, tokens and full payloads are not echoed.
  • Every code appears in the status and action table, with wait-versus-escalate spelled out for 429 and 5xx.
  • Someone who did not write the code has recovered from the message alone, timed.

Work through that list once for a service with a dozen error paths and the shape of the next hundred becomes obvious. The messages get shorter to write over time, because the template does the thinking, and the support queue gets quieter, because the reader already knows what to do.

FAQ

FAQ

How long should an error message be?

Long enough to carry the four parts and no longer: what failed, which input, why, and the fix. For APIs that is usually two sentences plus the field and code. For a CLI, one sentence plus a corrected command and a link. Trim adjectives and internal identifiers before trimming the fix line.

Should the error message include a stack trace?

No. Stack traces belong in server logs, correlated by the request ID. Return the trace identifier, not the trace, and put the diagnostic detail behind a link or a debug flag. Postman's guidance on not leaking sensitive data applies directly, since traces often contain paths, queries and payload fragments (Postman).

Where exactly should the docs link go?

To the specific reference anchor for that error code or parameter, on a page that includes a short correction example. Never to the docs home page or a search URL, because both add a navigation step while the reader is blocked. If the anchor does not exist yet, create it before shipping the code that links to it.

Do error codes need to be stable forever?

Treat them as append-only: adding new codes is safe, renaming or reusing a code breaks clients that branched on it. Speakeasy's design guidance shows how a code enables programmatic handling such as checking error.code === 'trip_too_short' (Speakeasy). If a code must be retired, keep it documented as deprecated and stop emitting it before you remove it.

How many people should test a new error message?

One person who did not write the code is the minimum, and it catches most gaps. Give them the error alone, ask what happened and what they will do next, and time the recovery. If they get stuck, the words they search for belong in the message or on the anchor page.

Sources

Sources

  1. Errors Best Practices in REST API Design - Speakeasy
  2. Best Practices for API Error Handling
  3. Error-Message Guidelines
  4. CLI Error Messages
  5. Diátaxis
  6. Start here - Diátaxis in five minutes
  7. Docs as Code - Write the Docs
  8. Why we use a 'docs as code' approach for technical documentation
  9. API Documentation Best Practices Guide - Stoplight.io
About the author

About the author

Antonio Evans
Antonio Evans

Antonio Evans is the founder of Quanta Intelligence and a fractional CTO who builds HIPAA-ready AI systems for health and life-science companies. Before that he was technical co-founder of FriendsEAT for twenty years, CEO of Socialdraft, and a data engineer at IBM. Antonio lives with his dog Santino in Lisbon, Portugal.

All articles