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)
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"
}
}
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
);
}
});
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
-
Never return 200 for errors. Some APIs do
200 {"success": false}— this breaks HTTP tooling. - Never expose stack traces in production. Log them server-side with the request_id.
- Use consistent error codes across your API. Document them.
- Include the request_id in every response (success too). It's the fastest path from "it's broken" to "here's why".
- 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)