Error Handling in REST APIs — Status Codes and Responses Guide
In this tutorial, you'll learn about Error Handling in REST APIs. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.
Error handling in REST APIs is the practice of returning consistent, informative HTTP status codes and structured error responses that help client developers understand and handle failures without guessing what went wrong.
What You'll Learn
You will learn proper HTTP status code selection, consistent error response formats, global error handler implementation, validation error reporting, and error logging strategies for production APIs.
Why Error Handling Matters
Poor error handling is the number one complaint from API consumers. Inconsistent error formats force developers to parse response bodies manually. Generic 500 errors hide real problems. Good error handling reduces integration time by 50 percent, improves developer experience, and makes debugging production issues faster.
Real-World Use
DodaTech APIs follow a consistent error response format across all products. Doda Browser sync API returns structured errors that the browser can display to users, DodaZIP update service includes retry hints in error responses, and Durga Antivirus Pro uses detailed error codes for SIEM integration.
Error Handling Learning Path
flowchart LR
A[REST API Design] --> B[Status Code Selection]
B --> C[Error Response Format]
C --> D[Global Error Handler]
D --> E[Validation Errors]
E --> F[Logging & Monitoring]
F --> G[Client SDK Generation]
B:::current
classDef current fill:#f90,color:#fff,stroke:#333,stroke-width:2px
Prerequisites
Understand RESTful Api Design Best Practices and HTTP Protocol Basics. Familiarity with JavaScript Basics or Python Basics is required for implementation examples.
HTTP Status Code Guide
2xx Success Codes
| Code | Meaning | When to Use |
|---|---|---|
| 200 OK | Request succeeded | GET, PUT, PATCH success |
| 201 Created | Resource created | POST success |
| 204 No Content | Success, no body | DELETE success |
| 202 Accepted | Request accepted asynchronously | Long-running operations |
3xx Redirection
| Code | Meaning | When to Use |
|---|---|---|
| 301 Moved Permanently | Resource URL changed permanently | API Migration |
| 302 Found | Temporary redirect | Load Balancing |
| 303 See Other | Redirect to another URL | After POST (PRG pattern) |
| 304 Not Modified | Resource not changed (cached) | Conditional GET with ETag |
4xx Client Errors
| Code | Meaning | When to Use |
|---|---|---|
| 400 Bad Request | Malformed request syntax | Invalid JSON, missing required fields |
| 401 Unauthorized | Authentication required | Missing or invalid auth token |
| 403 Forbidden | No permission | Authenticated but not authorized |
| 404 Not Found | Resource does not exist | Invalid resource ID |
| 405 Method Not Allowed | HTTP method not supported | POST on read-only endpoint |
| 409 Conflict | Resource State conflict | Duplicate resource, stale version |
| 410 Gone | Resource deleted permanently | Soft-deleted resources |
| 422 Unprocessable Entity | Validation error | Invalid field values, type errors |
| 429 Too Many Requests | Rate limit exceeded | Client exceeded API limits |
5xx Server Errors
| Code | Meaning | When to Use |
|---|---|---|
| 500 Internal Server Error | Unexpected server failure | Unhandled exceptions |
| 502 Bad Gateway | Upstream service failed | Proxy/gateway errors |
| 503 Service Unavailable | Server temporarily overloaded | Maintenance, traffic spikes |
| 504 Gateway Timeout | Upstream service timed out | Slow dependencies |
Consistent Error Response Format
Every error response should follow the same structure:
{
"status": 400,
"error": "validation_error",
"message": "Validation failed for the request",
"details": [
{
"field": "email",
"code": "invalid_email",
"message": "The provided email address is not valid",
"value": "not-an-email]
}
],
"requestId": "req_abc123",
"documentationUrl": "https://docs.dodatech.com/api/errors#validation_error",
"timestamp": "2026-06-23T10:00:00Z"
}
Error Response Fields
| Field | Required | Description |
|---|---|---|
| status | Yes | HTTP status code |
| error | Yes | Machine-readable error code |
| message | Yes | Human-readable error message |
| details | No | Array of field-level errors |
| requestId | Yes | Unique request identifier for debugging |
| documentationUrl | No | Link to error documentation |
| timestamp | Yes | When the error occurred |
Machine-Readable Error Codes
const ERROR_CODES = {
VALIDATION_ERROR: "validation_error",
NOT_FOUND: "not_found",
UNAUTHORIZED: "unauthorized",
FORBIDDEN: "forbidden",
CONFLICT: "conflict",
RATE_LIMITED: "rate_limited",
INTERNAL_ERROR: "internal_error",
SERVICE_UNAVAILABLE: "service_unavailable",
DEPENDENCY_FAILURE: "dependency_failure",
INVALID_STATE: "invalid_state"
};
Global Error Handler Implementation
Express.js Global Error Handler
// Create a custom error class
class ApiError extends Error {
constructor(statusCode, error, message, details = null) {
super(message);
this.statusCode = statusCode;
this.error = error;
this.details = details;
this.requestId = generateRequestId();
this.timestamp = new Date().toISOString();
}
}
// Error factory functions
const Errors = {
badRequest: (msg, details) =>
new ApiError(400, "bad_request", msg, details),
unauthorized: (msg = "Authentication required") =>
new ApiError(401, "unauthorized", msg),
forbidden: (msg = "Insufficient permissions") =>
new ApiError(403, "forbidden", msg),
notFound: (resource = "Resource") =>
new ApiError(404, "not_found", `${resource} not found`),
conflict: (msg) =>
new ApiError(409, "conflict", msg),
validationError: (details) =>
new ApiError(422, "validation_error", "Validation failed", details),
tooManyRequests: () =>
new ApiError(429, "rate_limited", "Rate limit exceeded, please try again later"),
internal: (msg = "Internal server error") =>
new ApiError(500, "internal_error", msg)
};
// Throw errors in controllers
app.get("/api/users/:id", async (req, res, next) => {
try {
const user = await userModel.findById(req.params.id);
if (!user) {
throw Errors.notFound("User");
}
res.json({ success: true, data: user });
} catch (error) {
next(error);
}
});
// Global error handler middleware
app.use((err, req, res, next) => {
// Log the error
console.error(`[${err.requestId}] ${err.statusCode || 500}: ${err.message}`);
// Determine status code
const statusCode = err.statusCode || 500;
const error = err.error || "internal_error";
const message = statusCode === 500
? "An unexpected error occurred"
: err.message;
// Build response
const response = {
status: statusCode,
error,
message,
requestId: err.requestId || generateRequestId(),
timestamp: err.timestamp || new Date().toISOString()
};
// Include details for validation errors
if (err.details) {
response.details = err.details;
}
// Include documentation URL for known errors
if (ERROR_DOCS[error]) {
response.documentationUrl = ERROR_DOCS[error];
}
// In development, include stack trace
if (process.env.NODE_ENV === "development") {
response.stack = err.stack;
}
res.status(statusCode).json(response);
});
FastAPI Global Error Handler
from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import JSONResponse
import uuid
from datetime import datetime
app = FastAPI()
class ApiError(Exception):
def __init__(self, status_code: int, error: str, message: str, details: list = None):
self.status_code = status_code
self.error = error
self.message = message
self.details = details
self.request_id = str(uuid.uuid4())
self.timestamp = datetime.utcnow().isoformat()
@app.exception_handler(ApiError)
async def api_error_handler(request: Request, exc: ApiError):
response = {
"status": exc.status_code,
"error": exc.error,
"message": exc.message,
"requestId": exc.request_id,
"timestamp": exc.timestamp
}
if exc.details:
response["details"] = exc.details
return JSONResponse(
status_code=exc.status_code,
content=response
)
@app.exception_handler(Exception)
async def general_exception_handler(request: Request, exc: Exception):
return JSONResponse(
status_code=500,
content={
"status": 500,
"error": "internal_error",
"message": "An unexpected error occurred",
"requestId": str(uuid.uuid4()),
"timestamp": datetime.utcnow().isoformat()
}
)
Validation Error Reporting
Field-Level Validation Errors
function validateCreateUser(body) {
const errors = [];
if (!body.name || body.name.trim().length < 2) {
errors.push({
field: "name",
code: "too_short",
message: "Name must be at least 2 characters",
value: body.name
});
}
if (!body.email || !isValidEmail(body.email)) {
errors.push({
field: "email",
code: "invalid_email",
message: "A valid email address is required",
value: body.email
});
}
if (!body.password || body.password.length < 8) {
errors.push({
field: "password",
code: "too_short",
message: "Password must be at least 8 characters"
});
}
return errors.length > 0 ? errors : null;
}
Expected Validation Error Response
{
"status": 422,
"error": "validation_error",
"message": "Validation failed",
"details": [
{
"field": "name",
"code": "too_short",
"message": "Name must be at least 2 characters",
"value": "]
},
{
"field": "email",
"code": "invalid_email",
"message": "A valid email address is required",
"value": "not-an-email"
}
],
"requestId": "req_xyz789",
"timestamp": "2026-06-23T10:00:00Z"
}
Error Logging Strategy
function logError(error, req) {
const logEntry = {
requestId: error.requestId,
timestamp: error.timestamp,
method: req.method,
path: req.path,
statusCode: error.statusCode || 500,
errorCode: error.error,
message: error.message,
userId: req.user?.id,
ip: req.ip,
userAgent: req.headers["user-agent"],
// Stack trace in development only
...(Process.env.NODE_ENV === "development" && { Stack: error.Stack })
};
// Send to logging service
logger.error(logEntry);
// Send critical errors to alerting
if (error.statusCode >= 500) {
alertingService.sendAlert(logEntry);
}
}
Common Errors
Returning 500 for client errors — Validation errors and bad requests should return 4xx codes, not 500. A 500 status tells the client the server is broken when the client actually sent bad data.
Inconsistent error format — Some endpoints return
{error: "msg"}, others return{message: "msg"}. Define a single error response format and use it everywhere.Leaking internal details — Returning database error messages, Stack traces, or internal IP addresses in error responses. Always sanitize error messages for production.
Missing error codes — Using only human-readable messages without machine-readable codes. Clients cannot programmatically handle errors without codes.
No request ID — Returning errors without a way to correlate them to server logs. Always include a unique request ID that developers can provide when reporting issues.
Not documenting error responses — Documenting only successful responses in OpenAPI spec. Every possible error response should be documented with examples.
Empty response bodies on errors — Returning only a status code with no body. Even for 204, you need headers. For all other status codes, include a structured error body.
Practice Questions
- What is the difference between 401 Unauthorized and 403 Forbidden?
- Why should error responses include a unique request ID?
- What status code should you return for a validation error?
- How do you implement a global error handler in Express.js?
- What fields should a consistent error response include?
Challenge
Implement a comprehensive error handling system for a REST API. Create a custom error class hierarchy, define 15+ machine-readable error codes, implement a global error handler with proper logging, add validation that returns field-level errors, document all error responses in OpenAPI spec, and create an error reference page in the documentation portal.
FAQ
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro