Software Engineering

Error Messages Are a Product Surface Engineers Keep Neglecting

Key takeaway: An error message is read at the worst possible moment by someone under pressure. Its job is to end the investigation, not to start one.

The Baseline Failure

Error: request failed tells the reader nothing they did not already know. It does not identify which request, which field, which constraint, or what to change. Every consumer of that message must reproduce the failure with additional instrumentation to learn anything.

Multiply that across a support queue and the cost is enormous — not in the milliseconds of handling the error, but in the hours spent by everyone downstream reconstructing information the system already had at the moment of failure.

What Belongs in an Error

Four elements, and the fourth is the one most often missing.

  1. What operation failed — specifically, including the identifier involved
  2. Why it failed — the actual constraint violated, not a category
  3. A stable machine-readable code so callers can branch without string matching
  4. What to do next — the remedy, or where to look for it

Compare:

{"error": "validation failed"}

against:

{
  "code": "INVALID_DATE_RANGE",
  "message": "end_date (2026-01-15) must be after start_date (2026-03-01)",
  "field": "end_date",
  "docs": "https://api.example.com/errors/INVALID_DATE_RANGE",
  "request_id": "req_8f2a91c4"
}

The second version ends the investigation. The caller knows the field, sees both offending values, has a code to branch on, and holds a request identifier that correlates to server logs.

The Two-Audience Problem

Errors have two readers with incompatible needs, and conflating them causes both security and usability problems.

The end user needs plain language and a next action. They must not see stack traces, SQL fragments, internal hostnames or table names — that is reconnaissance material for an attacker and noise for everyone else.

The operator needs the stack trace, the query, the upstream response and full context.

Resolve this with a correlation identifier. Return a request ID to the user with a generic-but-actionable message; log the full detail server-side keyed to that ID. Support can then retrieve everything from the identifier alone, without the system ever exposing internals.

Practical Rules

Never swallow an exception with a bare except: pass. If a failure is genuinely ignorable, log it at debug level with a comment explaining why — the silent variant guarantees that some future incident will have no trace of its origin.

Preserve the cause when wrapping. raise ProcessingError("could not parse invoice") from exc keeps the chain; re-raising a fresh exception discards the only evidence.

Include the values that triggered the failure, subject to redaction rules. “Timeout after 30s calling billing-service” is useful; “Timeout” is not.

And write the message for someone who does not know the codebase, because at 3 a.m. that describes everyone including its author.

The Bottom Line

Treat error text as a designed interface with a specification, not as an afterthought. Include what failed, why, a stable code and a next step; separate user-facing messages from operator detail using a correlation ID; and never let a failure disappear silently.

Related Articles

Leave a Reply

Your email address will not be published. Required fields are marked *

Back to top button