The Error Message That Tells the Developer Nothing at All

by Arif Ikhsanudin, Backend Developer

The message you shipped

{ "message": "Something went wrong" }

This is not an edge case. It ships in production APIs every week. Sometimes it comes dressed up:

{ "error": "Internal server error", "status": 500 }

Or with false precision:

{ "code": 1042, "message": "Operation failed" }

The code number implies a lookup table exists somewhere. It usually does not, or the table is internal and the developer has no access to it. Either way, the developer is stuck.

Why this keeps happening

The person who wrote the error handler knew exactly what went wrong. The exception was right there in the stack trace. The context was obvious from the surrounding code. So they wrote a message that made sense to them in that moment and moved on.

The problem is that the message has to make sense to someone who has none of that context, months or years later, under pressure.

There is also a habit of treating error messages as defensive — vague to avoid leaking internals. That instinct is not wrong, but it gets applied too broadly. The result is that legitimate integrators are treated the same as potential attackers.

The anatomy of a useless message

A message is useless when it:

  • Describes the category of failure without the cause ("Invalid input")
  • Names the system that failed without saying why ("Database error")
  • Gives a code without a stable reference ("Error 5023")
  • Is accurate but not actionable ("Request could not be completed")

None of these tell the developer what to fix. They tell them to go look somewhere else — logs, documentation, support tickets — for the real answer.

Rewriting useless messages

Take a common validation failure:

Before:

{ "error": "Validation error" }

After:

{
  "error": {
    "code": "VALIDATION_FAILED",
    "message": "The 'interval' field must be one of: daily, weekly, monthly. Received: 'bi-weekly'."
  }
}

The after version answers the question immediately. The developer does not need to look anything up.

A dependency timeout:

Before:

{ "error": "Service unavailable" }

After:

{
  "error": {
    "code": "DEPENDENCY_TIMEOUT",
    "message": "The downstream inventory service did not respond within the 5000ms timeout. This is likely transient.",
    "retryable": true
  }
}

Now the developer knows it is not their fault, knows not to change their request, and knows to retry.

An authorization failure:

Before:

{ "error": "Forbidden" }

After:

{
  "error": {
    "code": "INSUFFICIENT_SCOPE",
    "message": "This endpoint requires the 'orders:write' scope. The token provided has scopes: ['orders:read', 'products:read']."
  }
}

The developer now knows exactly which scope to request. No support ticket needed.

The security objection

The common pushback: detailed messages expose internals. True, with caveats.

For authenticated API consumers — developers building on your platform — detailed errors are almost always the right call. They are already inside your trust boundary.

For unauthenticated or public-facing endpoints, be selective. You do not need to expose which specific database constraint failed on a signup endpoint. But you can still be specific without being insecure:

{
  "error": {
    "code": "EMAIL_ALREADY_REGISTERED",
    "message": "An account with this email address already exists."
  }
}

This is specific and actionable. Yes, it confirms whether an email is registered — but that same information is available via the "forgot password" flow. Obscuring it in the signup error does not meaningfully improve security while it definitely degrades the developer experience.

The question to ask: does this message help a legitimate developer more than it helps an attacker? For most API errors, the answer is yes.

Write the message for the person at 11pm

The heuristic that works: write the error message assuming the reader has no access to your codebase, your logs, or your documentation. They have only what you return. If that message does not give them a path forward, rewrite it until it does.

That constraint will surface every vague, defensive, or lazy message in your API. Fix them before you ship.

Scale Your Backend - Need an Experienced Backend Developer?

We provide backend engineers who join your team as contractors to help build, improve, and scale your backend systems.

We focus on clean backend design, clear documentation, and systems that remain reliable as products grow. Our goal is to strengthen your team and deliver backend systems that are easy to operate and maintain.

We work from our own development environments and support teams across US, EU, and APAC timezones. Our workflow emphasizes documentation and asynchronous collaboration to keep development efficient and focused.

  • Production Backend Experience. Experience building and maintaining backend systems, APIs, and databases used in production.
  • Scalable Architecture. Design backend systems that stay reliable as your product and traffic grow.
  • Contractor Friendly. Flexible engagement for short projects, long-term support, or extra help during releases.
  • Focus on Backend Reliability. Improve API performance, database stability, and overall backend reliability.
  • Documentation-Driven Development. Development guided by clear documentation so teams stay aligned and work efficiently.
  • Domain-Driven Design. Design backend systems around real business processes and product needs.

Tell us about your project

Our offices

  • Copenhagen
    1 Carlsberg Gate
    1260, København, Denmark
  • Magelang
    12 Jalan Bligo
    56485, Magelang, Indonesia

More articles

If Your API Needs a Long Explanation It Is Probably Too Complex

An API that requires extensive documentation to use is an API whose complexity has been transferred to the consumer. Simplicity is a design goal, not a constraint.

Read more

NULL in SQL Does Not Mean What You Think It Means

NULL represents the absence of a value, not zero, not an empty string, and not false — its three-valued logic and propagation rules produce query results that are consistently surprising to developers who treat it as a regular value.

Read more

How I Manage My Energy as a Developer, Not Just My Time

Time management systems assume all hours are equal. They aren't — and once you start treating your energy as the scarce resource instead of your calendar, the way you work changes considerably.

Read more

Optimistic Locking in Hibernate — @Version, Retry Strategies, and Conflict Resolution

Concurrent updates to the same entity without coordination produce lost updates — the last write wins and intermediate changes are silently discarded. Optimistic locking detects this at commit time. Here is how it works and how to handle the conflicts it surfaces.

Read more