api-error-handling
Implement comprehensive API error handling with standardized error responses, logging, monitoring, and user-friendly messages. Use when building resilient APIs, debugging issues, or improving error reporting.
Installation
Copy to your project
cp -r skills/api-error-handling/ /your-project/.claude/skills/api-error-handling/
API Error Handling
Overview
Build robust error handling systems with standardized error responses, detailed logging, error categorization, and user-friendly error messages.
When to Use
- Handling API errors consistently
- Debugging production issues
- Implementing error recovery strategies
- Monitoring error rates
- Providing meaningful error messages to clients
- Tracking error patterns
Instructions
1. Standardized Error Response Format
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Input validation failed",
"statusCode": 422,
"requestId": "req_abc123xyz789",
"timestamp": "2025-01-15T10:30:00Z",
"details": [
{
"field": "email",
"message": "Invalid email format",
"code": "INVALID_EMAIL"
},
{
"field": "age",
"message": "Must be at least 18",
"code": "VALUE_OUT_OF_RANGE"
}
],
"path": "/api/users",
"method": "POST",
"traceId": "trace_001"
}
}
2. Node.js Error Handling
const express = require("express");
const app = express();
// Error codes and mappings
const ERROR_CODES = {
VALIDATION_ERROR: { status: 422, message: "Validation failed" },
NOT_FOUND: { status: 404, message: "Resource not found" },
UNAUTHORIZED: { status: 401, message: "Authentication required" },
FORBIDDEN: { status: 403, message: "Access denied" },
CONFLICT: { status: 409, message: "Resource conflict" },
RATE_LIMITED: { status: 429, message: "Too many requests" },
INTERNAL_ERROR: { status: 500, message: "Internal server error" },
SERVICE_UNAVAILABLE: { status: 503, message: "Service unavailable" },
};
// Custom error class
class ApiError extends Error {
constructor(code, message, statusCode = null, details = null) {
super(message);
this.code = code;
this.statusCode = statusCode || ERROR_CODES[code]?.status || 500;
this.details = details;
this.timestamp = new Date().toISOString();
}
}
// Global error handler middleware
app.use((err, req, res, next) => {
const requestId = req.id || `req_${Date.now()}`;
const traceId = req.traceId;
// Log error
logError(err, {
requestId,
traceId,
method: req.method,
path: req.path,
query: req.query,
userId: req.user?.id,
});
// Handle different error types
if (err instanceof ApiError) {
return res
.status(err.statusCode)
.json(formatErrorResponse(err, requestId, traceId));
}
if (err instanceof SyntaxError && "body" in err) {
const apiError = new ApiError("VALIDATION_ERROR", "Invalid JSON", 400);
return res
.status(400)
.json(formatErrorResponse(apiError, requestId, traceId));
}
if (err.name === "ValidationError") {
const details = Object.keys(err.errors).map((field) => ({
field,
message: err.errors[field].message,
code: "VALIDATION_FAILED",
}));
const apiError = new ApiError(
"VALIDATION_ERROR",
"Validation failed",
422,
details,
);
return res
.status(422)
.json(formatErrorResponse(apiError, requestId, traceId));
}
if (err.name === "CastError") {
const apiError = new ApiError("NOT_FOUND", "Invalid resource ID", 404);
return res
.status(404)
.json(formatErrorResponse(apiError, requestId, traceId));
}
// Unknown error
const internalError = new ApiError(
"INTERNAL_ERROR",
"An unexpected error occurred",
500,
);
res.status(500).json(formatErrorResponse(internalError, requestId, traceId));
});
// Error response formatter
function formatErrorResponse(error, requestId, traceId) {
return {
error: {
code: error.code,
message: error.message,
statusCode: error.statusCode,
requestId,
timestamp: error.timestamp,
...(error.details && { details: error.details }),
traceId,
},
};
}
// Error logger
function logError(error, context) {
const logData = {
timestamp: new Date().toISOString(),
errorCode: error.code,
errorMessage: error.message,
statusCode: error.statusCode,
stack: error.stack,
context,
};
// Log to different levels based on severity
if (error.statusCode >= 500) {
console.error("[ERROR]", JSON.stringify(logData));
// Send to error tracking service (Sentry, etc)
trackError(logData);
} else if (error.statusCode >= 400) {
console.warn("[WARN]", JSON.stringify(logData));
}
}
// Route with error handling
app.post("/api/users", async (req, res, next) => {
try {
const { email, firstName, lastName } = req.body;
// Validation
if (!email || !firstName || !lastName) {
throw new ApiError(
"VALIDATION_ERROR",
"Missing required fields",
422,
[
!email && { field: "email", message: "Email is required" },
!firstName && {
field: "firstName",
message: "First name is required",
},
!lastName && { field: "lastName", message: "Last name is required" },
].filter(Boolean),
);
}
// Check for conflicts
const existing = await User.findOne({ email });
if (existing) {
throw new ApiError("CONFLICT", "Email already exists", 409);
}
const user = await User.create({ email, firstName, lastName });
res.status(201).json({ data: user });
} catch (error) {
next(error);
}
});
// Async route wrapper
const asyncHandler = (fn) => (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
app.get(
"/api/users/:id",
asyncHandler(async (req, res) => {
const user = await User.findById(req.params.id);
if (!user) {
throw new ApiError("NOT_FOUND", "User not found", 404);
}
res.json({ data: user });
}),
);
// Handle unhandled rejections
process.on("unhandledRejection", (reason, promise) => {
console.error("Unhandled Rejection:", reason);
trackError({ type: "unhandledRejection", reason });
});
3. Python Error Handling (Flask)
from flask import Flask, jsonify, request
from datetime import datetime
import logging
import traceback
from functools import wraps
app = Flask(__name__)
logger = logging.getLogger(__name__)
class APIError(Exception):
def __init__(self, code, message, status_code=500, details=None):
super().__init__()
self.code = code
self.message = message
self.status_code = status_code
self.details = details or []
self.timestamp = datetime.utcnow().isoformat()
ERROR_CODES = {
'VALIDATION_ERROR': 422,
'NOT_FOUND': 404,
'UNAUTHORIZED': 401,
'FORBIDDEN': 403,
'CONFLICT': 409,
'INTERNAL_ERROR': 500
}
def format_error(error, request_id, trace_id):
return {
'error': {
'code': error.code,
'message': error.message,
'statusCode': error.status_code,
'requestId': request_id,
'timestamp': error.timestamp,
'traceId': trace_id,
'details': error.details if error.details else None
}
}
@app.errorhandler(APIError)
def handle_api_error(error):
request_id = request.headers.get('X-Request-ID', f'req_{int(datetime.utcnow().timestamp())}')
trace_id = request.headers.get('X-Trace-ID')
log_error(error, {
'request_id': request_id,
'trace_id': trace_id,
'method': request.method,
'path': request.path
})
response = jsonify(format_error(error, request_id, trace_id))
return response, error.status_code
@app.errorhandler(400)
def handle_bad_request(error):
request_id = f'req_{int(datetime.utcnow().timestamp())}'
api_error = APIError('VALIDATION_ERROR', 'Invalid request', 400)
return jsonify(format_error(api_error, request_id, None)), 400
@app.errorhandler(404)
def handle_not_found(error):
request_id = f'req_{int(datetime.utcnow().timestamp())}'
api_error = APIError('NOT_FOUND', 'Resource not found', 404)
return jsonify(format_error(api_error, request_id, None)), 404
@app.errorhandler(500)
def handle_internal_error(error):
request_id = f'req_{int(datetime.utcnow().timestamp())}'
logger.error(f'Internal error: {error}', exc_info=True)
api_error = APIError('INTERNAL_ERROR', 'Internal server error', 500)
return jsonify(format_error(api_error, request_id, None)), 500
def log_error(error, context):
log_entry = {
'timestamp': datetime.utcnow().isoformat(),
'code': error.code,
'message': error.message,
'status': error.status_code,
'context': context
}
if error.status_code >= 500:
logger.error(log_entry)
elif error.status_code >= 400:
logger.warning(log_entry)
@app.route('/api/users', methods=['POST'])
def create_user():
data = request.get_json()
if not data:
raise APIError('VALIDATION_ERROR', 'Request body required', 400)
errors = []
if not data.get('email'):
errors.append({'field': 'email', 'message': 'Email is required'})
if not data.get('firstName'):
errors.append({'field': 'firstName', 'message': 'First name is required'})
if errors:
raise APIError('VALIDATION_ERROR', 'Validation failed', 422, errors)
try:
user = User.create(**data)
return jsonify({'data': user.to_dict()}), 201
except IntegrityError:
raise APIError('CONFLICT', 'Email already exists', 409)
@app.route('/api/users/<user_id>')
def get_user(user_id):
user = User.query.get(user_id)
if not user:
raise APIError('NOT_FOUND', 'User not found', 404)
return jsonify({'data': user.to_dict()})
4. Error Recovery Strategies
// Circuit breaker pattern
class CircuitBreaker {
constructor(failureThreshold = 5, timeout = 60000) {
this.failureCount = 0;
this.failureThreshold = failureThreshold;
this.timeout = timeout;
this.state = "CLOSED"; // CLOSED, OPEN, HALF_OPEN
this.nextAttempt = Date.now();
}
async execute(fn) {
if (this.state === "OPEN") {
if (Date.now() < this.nextAttempt) {
throw new ApiError(
"SERVICE_UNAVAILABLE",
"Circuit breaker is open",
503,
);
}
this.state = "HALF_OPEN";
}
try {
const result = await fn();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
onSuccess() {
this.failureCount = 0;
this.state = "CLOSED";
}
onFailure() {
this.failureCount++;
if (this.failureCount >= this.failureThreshold) {
this.state = "OPEN";
this.nextAttempt = Date.now() + this.timeout;
}
}
}
// Retry with exponential backoff
async function retryWithBackoff(fn, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
if (attempt === maxRetries - 1) throw error;
const delay = Math.pow(2, attempt) * 1000;
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
}
5. Error Monitoring
// Sentry integration
const Sentry = require("@sentry/node");
Sentry.init({ dsn: process.env.SENTRY_DSN });
function trackError(errorData) {
Sentry.captureException(new Error(errorData.errorMessage), {
tags: {
code: errorData.errorCode,
status: errorData.statusCode,
},
extra: errorData.context,
});
}
// Error rate monitoring
const errorMetrics = {
total: 0,
byCode: {},
byStatus: {},
};
function recordError(error) {
errorMetrics.total++;
errorMetrics.byCode[error.code] = (errorMetrics.byCode[error.code] || 0) + 1;
errorMetrics.byStatus[error.statusCode] =
(errorMetrics.byStatus[error.statusCode] || 0) + 1;
}
app.get("/metrics/errors", (req, res) => {
res.json(errorMetrics);
});
Best Practices
✅ DO
- Use consistent error response format
- Include request ID for tracing
- Log with appropriate severity levels
- Provide actionable error messages
- Include error details for debugging
- Use standard HTTP status codes
- Implement error recovery strategies
- Monitor error rates
- Distinguish user vs server errors
- Handle all error types
❌ DON’T
- Expose stack traces to clients
- Return 200 for errors
- Ignore errors silently
- Log sensitive data
- Use vague error messages
- Mix error handling with business logic
- Retry all errors indefinitely
- Expose internal implementation details
- Return different formats for errors