The Error Message That Tells the Developer Nothing at All

by Eric Hanson, Backend Developer at Clean Systems Consulting

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

Testing Rails APIs with RSpec — My Practical Approach

Request specs in Rails test the full stack efficiently, but most teams either over-test at the wrong layer or under-test the cases that matter. Here is the structure that finds real bugs without slowing the suite down.

Read more

API Gateways in Spring Boot — What They Do, When You Need One, and How to Configure Spring Cloud Gateway

An API gateway is a single entry point that handles cross-cutting concerns — routing, authentication, rate limiting, and observability — so individual services don't have to. Spring Cloud Gateway is the Spring-native implementation. Here is what it solves and how to configure it.

Read more

Why London Startups Are Quietly Moving Backend Work to Async Remote Contractors

Your engineer quit last month. You still haven't replaced them. Maybe you don't need to.

Read more

7 Essential Insurances Every Remote Contractor Should Have

Remote contractors focus on results, not office presence. With fewer meetings and clearer scope, work moves faster and more efficiently.

Read more