Back to Blog
documentation best-practices api tutorial

Writing error messages and troubleshooting docs users don't hate

Generic error codes create support tickets. Learn to write error messages and troubleshooting docs that tell developers what broke and how to fix it.

Yorjander Hernandez
Yorjander Hernandez
Co-founder & CTO · · 7 min read
Writing error messages and troubleshooting docs users don't hate

“Error 400: Bad Request” tells a developer nothing they didn’t already know from the status code. They knew the request was bad — that’s why they’re staring at your docs instead of shipping. What they need is what field was wrong, what a valid value looks like, and where to fix it. A status code without that is not an error message. It’s a shrug.

Why “Error 400: Bad Request” is a documentation failure

Most teams treat error messages as an engineering afterthought and troubleshooting docs as a support afterthought. Neither gets a writer’s attention until the ticket volume forces it. By then the error strings are frozen into client SDKs, the docs are three releases behind, and support is copy-pasting the same Slack answer for the fortieth time.

The cost is not abstract. Every generic error message generates one of two outcomes:

  • The developer files a support ticket, and someone on your team spends ten minutes doing what a well-written response could have done in zero.
  • The developer gives up on your API entirely and picks a competitor whose docs told them what to do.

Neither outcome shows up as a “documentation bug” in your tracker. It shows up as churn, as a support backlog, as an integration that stalls in week one. The error message is the documentation — it’s the doc your user actually reads, at the exact moment they’re motivated to read it. Treating it as a throwaway string is the failure, not a rounding error.

The fix isn’t “write better copy.” It’s treating your error responses and troubleshooting pages as a single system: the API tells the developer what broke, and hands them a direct line to the page that explains why and how to fix it.

The anatomy of a useful error — what happened, why, how to fix it

A useful error answers three questions, in order, every time:

  1. What happened. Precisely — not “invalid input” but which field, which constraint, which value.
  2. Why it happened. The rule that was violated, stated as a rule, not a stack trace.
  3. How to fix it. A concrete next step: the correct format, a link to the relevant doc, or both.

Compare the two responses below for a webhook registration endpoint that rejects a malformed URL.

❌ Bad:

{
  "error": "Bad Request",
  "code": 400
}

✅ Good:

{
  "error": {
    "code": "invalid_webhook_url",
    "message": "The 'url' field must be an HTTPS endpoint. Received: 'http://example.com/hook'.",
    "field": "url",
    "docs_url": "https://docs.gitdoc.ai/errors/invalid_webhook_url",
    "request_id": "req_8f3a2c91"
  }
}

The second response costs almost nothing extra to generate — the API already knows the field, the value, and the rule it failed. What it takes is deciding, at design time, that the error object is a documentation surface, not just a status flag. A stable code (not just an HTTP status) lets developers pattern-match and handle errors programmatically. The message is human-readable and specific enough to act on without opening a browser tab. The field pinpoints the exact key so a developer with a 40-field payload doesn’t have to eyeball it. The request_id matters just as much as the message — it’s what turns “it’s broken” into a ticket support can actually trace.

None of this requires guessing what confuses users. It requires looking at what you already know at the point of failure and refusing to throw it away.

Structuring a troubleshooting page developers can scan under pressure

Nobody reads a troubleshooting page start to finish. They land on it from a search, a linked docs_url, or a panicked Ctrl+F, already holding a specific error in their hand. The page has to be scannable in the state of mind of someone whose deploy is failing.

Structure each error entry the same way, every time, so developers learn the pattern once and never have to relearn it:

  1. The exact error code and message as a heading — copy-pasteable, matching what they see in their terminal or response body verbatim.
  2. One-sentence cause — the rule that was violated, no preamble.
  3. The fix, as numbered steps if there’s more than one action, or a single corrected example if there’s one.
  4. A minimal before/after code snippet where the fix is non-obvious.
  5. A link to the related concept page (auth, webhooks, rate limits) for the developer who needs the deeper context, not just the fix.

Keep each entry short enough to fit on one screen. If an error needs more than that, it’s not an error page problem — it’s a sign the underlying concept needs its own doc, and the error entry should link out to it rather than absorb it. A troubleshooting page that tries to also be a tutorial fails at both jobs.

Group entries by the part of the system they belong to (authentication, rate limits, webhooks, payloads) rather than by HTTP status code. “400 errors” is how your server thinks about the problem; “why is my webhook signature invalid” is how your user thinks about it. Organize the page around their mental model, not yours.

Linking error codes directly to their docs (from the API response itself)

The best troubleshooting doc is the one a developer never has to search for, because the API handed them the URL directly. This is the single highest-leverage change most API teams can make to their error handling, and almost none of them do it.

The pattern is simple: every error response includes a docs_url field that points to the exact entry for that error code — not your docs homepage, not a generic “errors” index, the specific anchor for invalid_webhook_url. That means:

  • Your error taxonomy (the list of code values your API can return) and your troubleshooting page need to share a single source of truth, so a new error code can’t ship without a corresponding doc entry existing before the code does.
  • The URL needs to be stable. If you restructure your docs site and an old docs_url 404s, you’ve turned a helpful link into a worse experience than not having one.
  • SDKs and CLI tools should surface the link prominently in error output, not bury it in a JSON blob a developer has to console.log to find.

This is also where keeping docs and code in sync stops being optional. An error code with no matching doc entry, or a docs_url that points at stale content describing a parameter you renamed two releases ago, is worse than a generic message — it actively wastes the developer’s trust along with their time. This is a chunk of what we built GitDoc (gitdoc.ai) to solve: when your error codes or API surface change, the docs that describe them update automatically instead of drifting until a support ticket catches it.

Turning support tickets into your troubleshooting doc backlog

Your support queue is a ranked list of the errors your docs currently fail to prevent. Most teams don’t read it that way — tickets get resolved one at a time, the answer lives in a Slack thread or a support macro, and the same question comes back in three weeks from a different developer who hit the identical wall.

Turn the queue into a backlog with a lightweight loop:

  1. Tag every support ticket with the error code involved, if there is one. If there isn’t a code — if the ticket is “your API doesn’t work” — that’s itself a signal the error response needs a code and a message, not just a status.
  2. Run a weekly count by code. The top five by volume are your next five troubleshooting entries or your next five doc rewrites, ranked by actual demand instead of guesswork.
  3. When support writes a canned response more than twice, promote it to a doc entry and reply with a link instead of retyping the answer. The support team’s shortcut becomes the public fix.
  4. Watch for tickets that resolve with “oh, turns out I just needed to X” — that “X” belongs in the “how to fix it” section of the relevant error entry, verbatim, because it’s already been field-tested on a real confused developer.
  5. Re-check resolved tickets after a doc update ships to confirm the volume for that code actually drops. If it doesn’t, the fix in the docs isn’t matching the real cause, and the ticket tag pointed you at the wrong root issue.

This loop only works if updating the docs is cheap enough to happen every week, not just during a quarterly cleanup. That’s the same structural problem as any other kind of doc rot: if writing the fix takes longer than answering the ticket by hand, support will keep answering it by hand forever, and the backlog never shrinks.

GitDoc keeps your docs in sync with your codebase on every push, so error codes, parameters, and the troubleshooting pages that reference them don’t quietly drift apart. Start free →

Keep reading