Skip to content
Tech

Designing Clear API Error Responses That Developers Can Act On

Vague errors waste hours. A practical guide to consistent error formats, the right HTTP status codes, validation messages and machine-readable codes.

Editorial illustration of a friendly warning sign beside neatly organised response cards on a monitor
Editorial illustration of a friendly warning sign beside neatly organised response cards on a monitor
In this article

    Developers judge an API by how it behaves when things go wrong. A response that says only "Something went wrong" with a 500 status sends them digging through documentation and support tickets. Clear, consistent error responses save time for everyone, including your own support team.

    Use HTTP status codes correctly

    Status codes are the first signal a client reads, and many libraries branch on them automatically.

    • 400 Bad Request: the request is malformed, such as invalid JSON.
    • 401 Unauthorized: authentication is missing or invalid.
    • 403 Forbidden: the client is authenticated but not allowed to do this.
    • 404 Not Found: the resource does not exist, or the client should not know it exists.
    • 409 Conflict: the request conflicts with current state, such as a duplicate.
    • 422 Unprocessable Content: the request is well-formed but fails validation.
    • 429 Too Many Requests: rate limit exceeded; include a Retry-After header.
    • 500 and 503: server-side failures the client did not cause.

    Never return a 200 status with an error inside the body. It breaks monitoring, retries and client error handling.

    Adopt one consistent format

    Every error from every endpoint should share the same shape. The Problem Details standard (RFC 9457, which updated RFC 7807) is a good base, and you can extend it.

    {
      "type": "https://api.example.com/errors/insufficient-balance",
      "title": "Insufficient balance",
      "status": 409,
      "detail": "The wallet balance is lower than the transfer amount.",
      "code": "insufficient_balance",
      "request_id": "7f3c9a2e"
    }

    Include a stable machine-readable code

    Human-readable messages change as you improve wording or add translations. Clients should never parse them. Provide a short, stable code such as card_expired or email_taken that developers can switch on safely. Document every code and treat removing one as a breaking change.

    Return field-level validation errors

    When a form submission fails, tell the client exactly which fields are wrong and why, all at once rather than one per request.

    {
      "status": 422,
      "code": "validation_failed",
      "errors": {
        "email": ["The email has already been taken."],
        "phone": ["The phone number must be 10 digits."]
      }
    }

    Laravel's validator already produces this structure, so map it into your standard envelope rather than inventing a second format.

    Be helpful without leaking internals

    Explain what the client can do to fix the problem, but never expose stack traces, SQL queries, file paths or server versions in production. Those details belong in your logs. Include a request ID in the response so you can find the full context when a developer contacts support.

    Be careful with security-sensitive errors

    Some errors should be deliberately vague. A login endpoint should say "Invalid email or password" rather than revealing which part was wrong. Returning 404 instead of 403 for resources a user should not know about prevents enumeration.

    Make retries obvious

    Clients need to know whether trying again might help. Rate limits and temporary outages should include a Retry-After header. Validation and permission errors will fail again until the request changes. Some teams add a retryable boolean to make this explicit.

    Centralise error handling

    Rather than formatting errors in every controller, convert exceptions into responses in one place, such as Laravel's exception handler. Custom exception classes can carry their status, code and message, which keeps controllers clean and responses consistent.

    Document and test errors

    List possible errors for each endpoint in your API reference, with example bodies. Write tests that assert status codes and error codes for common failure paths, so a refactor does not quietly change behaviour clients depend on.

    Wrapping up

    Good error responses use the right status code, follow one consistent format, include stable codes and field-level details, help without leaking internals and make retry behaviour clear. They turn frustrating failures into quick fixes.

    Written by Raj

    Founder & Editor

    I write HorizonDesk: practical, no-hype articles on technology, money, business and the vehicles we drive. Everything here is something I’ve researched, tested or wanted explained clearly myself.