DEV Community

EME GUG
EME GUG

Posted on

Writing better API error responses

Most APIs return errors like {"error": "Something went wrong"}. That's not helpful for anyone — not the frontend dev, not the user, not the on-call engineer debugging at 3 AM.

The Problem

// Bad
{"error": "Invalid request"}

// Also bad
{"message": "Error", "code": 400}

// Worst
500 Internal Server Error (empty body)
Enter fullscreen mode Exit fullscreen mode

A Better Format

{
    "error": {
        "code": "VALIDATION_ERROR",
        "message": "Email address is invalid",
        "details": [
            {
                "field": "email",
                "message": "Must be a valid email address",
                "value": "not-an-email"
            }
        ],
        "request_id": "req_abc123"
    }
}
Enter fullscreen mode Exit fullscreen mode

Why each field matters:

  • code: Machine-readable, stable across versions. Frontend uses this to show the right UI.
  • message: Human-readable, can change. For developer debugging.
  • details: Field-level errors for forms. Frontend maps these to input fields.
  • request_id: Links to server logs. Makes debugging 10x faster.

Implementation (Express.js)

class AppError extends Error {
    constructor(code, message, statusCode = 400, details = null) {
        super(message);
        this.code = code;
        this.statusCode = statusCode;
        this.details = details;
    }
}

// Middleware
app.use((err, req, res, next) => {
    const requestId = req.id || crypto.randomUUID();

    if (err instanceof AppError) {
        return res.status(err.statusCode).json({
            error: {
                code: err.code,
                message: err.message,
                details: err.details,
                request_id: requestId
            }
        });
    }

    // Unknown error — don't leak internals
    console.error(`[${requestId}]`, err);
    res.status(500).json({
        error: {
            code: "INTERNAL_ERROR",
            message: "An unexpected error occurred",
            request_id: requestId
        }
    });
});

// Usage
app.post("/users", (req, res) => {
    const errors = validateUser(req.body);
    if (errors.length > 0) {
        throw new AppError(
            "VALIDATION_ERROR",
            "Invalid user data",
            400,
            errors
        );
    }
});
Enter fullscreen mode Exit fullscreen mode

HTTP Status Codes That Matter

Code When
400 Validation error, bad input
401 Not authenticated
403 Authenticated but not authorized
404 Resource not found
409 Conflict (duplicate email, version mismatch)
422 Semantically invalid (valid JSON but wrong values)
429 Rate limited
500 Server bug

Rules

  1. Never return 200 for errors. Some APIs do 200 {"success": false} — this breaks HTTP tooling.
  2. Never expose stack traces in production. Log them server-side with the request_id.
  3. Use consistent error codes across your API. Document them.
  4. Include the request_id in every response (success too). It's the fastest path from "it's broken" to "here's why".
  5. Validate early, fail fast. Don't process half the request then error on field 7.

How does your API handle errors? I'd love to see other patterns.

Top comments (0)