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)
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)
```javascript
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
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.
All Rights Reserved