Error Handling Best Practices
Effective error handling is crucial for building robust API integrations. This guide covers best practices for handling API errors gracefully in your applications.
In this guide:
- Understanding API Errors
- Common HTTP Status Codes
- Client-Side Error Handling
- Server-Side Error Handling
- Error Logging and Monitoring
- User-Friendly Error Messages
- Retry Strategies
Understanding API Errors
API errors can occur for various reasons, including:
- Invalid input or parameters
- Authentication or authorization failures
- Resource not found
- Rate limiting or quota exceeded
- Server-side issues
- Network problems
Understanding the different types of errors and how to handle them appropriately is essential for building resilient applications.
Common HTTP Status Codes
HTTP status codes are standardized codes returned by a server in response to a client's request. They are grouped into five classes:
| Code Range | Category | Description |
|---|---|---|
| 1xx | Informational | Request received, continuing process |
| 2xx | Success | Request successfully received, understood, and accepted |
| 3xx | Redirection | Further action needs to be taken to complete the request |
| 4xx | Client Error | Request contains bad syntax or cannot be fulfilled |
| 5xx | Server Error | Server failed to fulfill a valid request |
Common error status codes you'll encounter when working with APIs:
| Status Code | Name | Description |
|---|---|---|
| 400 | Bad Request | The request was malformed or contains invalid parameters |
| 401 | Unauthorized | Authentication is required and has failed or not been provided |
| 403 | Forbidden | The server understood the request but refuses to authorize it |
| 404 | Not Found | The requested resource could not be found |
| 429 | Too Many Requests | The user has sent too many requests in a given amount of time (rate limiting) |
| 500 | Internal Server Error | The server encountered an unexpected condition |
| 503 | Service Unavailable | The server is not ready to handle the request (temporary overload or maintenance) |
Client-Side Error Handling
Proper client-side error handling is essential for creating a good user experience. Here are some best practices:
Basic Error Handling
At a minimum, always check if the response was successful before processing it:
// Basic error handling with fetch
fetch('https://api.example.com/data')
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json();
})
.then(data => {
console.log('Success:', data);
})
.catch(error => {
console.error('Error:', error);
});Detailed Error Handling
For more robust error handling, consider different types of errors and handle them appropriately:
// More detailed error handling
async function fetchData() {
try {
const response = await fetch('https://api.example.com/data');
// Check for HTTP errors
if (!response.ok) {
// Try to parse error response
let errorMessage;
try {
const errorData = await response.json();
errorMessage = errorData.message || errorData.error || `HTTP error! Status: ${response.status}`;
} catch (e) {
errorMessage = `HTTP error! Status: ${response.status}`;
}
throw new Error(errorMessage);
}
const data = await response.json();
return data;
} catch (error) {
// Handle network errors
if (error.name === 'TypeError' && error.message === 'Failed to fetch') {
console.error('Network error: Please check your connection');
// Implement retry logic or fallback
} else {
console.error('Error:', error.message);
}
// Rethrow or handle appropriately
throw error;
}
}Implementing Retry Logic
For transient errors (like network issues or rate limiting), implementing retry logic with exponential backoff can improve reliability:
// Implementing retry logic
async function fetchWithRetry(url, options = {}, retries = 3, backoff = 300) {
try {
const response = await fetch(url, options);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.message || `HTTP error! Status: ${response.status}`);
}
return await response.json();
} catch (error) {
// Check if we should retry
if (retries > 0) {
// Check if error is retryable (e.g., 429, 503, network errors)
const isRetryable =
error.name === 'TypeError' || // Network error
(error.message && error.message.includes('429')) || // Too Many Requests
(error.message && error.message.includes('503')); // Service Unavailable
if (isRetryable) {
console.log(`Retrying... Attempts remaining: ${retries}`);
// Wait for backoff duration
await new Promise(resolve => setTimeout(resolve, backoff));
// Exponential backoff
return fetchWithRetry(url, options, retries - 1, backoff * 2);
}
}
// If we're out of retries or error is not retryable
throw error;
}
}Custom Error Classes
Creating custom error classes can help standardize error handling across your application:
// Creating custom error classes
class APIError extends Error {
constructor(message, status, code, data = null) {
super(message);
this.name = 'APIError';
this.status = status;
this.code = code;
this.data = data;
}
isClientError() {
return this.status >= 400 && this.status < 500;
}
isServerError() {
return this.status >= 500;
}
isAuthError() {
return this.status === 401 || this.status === 403;
}
isRateLimitError() {
return this.status === 429;
}
}
// Using the custom error class
async function fetchData() {
try {
const response = await fetch('https://api.example.com/data');
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new APIError(
errorData.message || 'An error occurred',
response.status,
errorData.code || 'unknown_error',
errorData
);
}
return await response.json();
} catch (error) {
if (error instanceof APIError) {
if (error.isAuthError()) {
console.error('Authentication error:', error.message);
// Redirect to login or refresh token
} else if (error.isRateLimitError()) {
console.error('Rate limit exceeded:', error.message);
// Implement backoff strategy
} else if (error.isServerError()) {
console.error('Server error:', error.message);
// Show appropriate UI message
} else {
console.error('API error:', error.message);
// Handle other API errors
}
} else {
console.error('Network or parsing error:', error);
// Handle network errors
}
throw error;
}
}Server-Side Error Handling
When building APIs, it's important to provide clear and consistent error responses:
- Use appropriate HTTP status codes
- Include error messages that are helpful but don't expose sensitive information
- Consider including error codes that clients can use for programmatic handling
- Include request IDs to help with debugging and support
Example Error Response Format
{
"error": {
"status": 400,
"code": "invalid_parameter",
"message": "The 'email' parameter is not a valid email address",
"details": {
"parameter": "email",
"value": "not-an-email",
"constraint": "email"
},
"requestId": "req_1234567890"
}
}Error Logging and Monitoring
Proper error logging and monitoring are essential for identifying and resolving issues:
- Log all errors with relevant context (request details, user information, etc.)
- Use structured logging for easier parsing and analysis
- Set up alerts for critical errors
- Use error monitoring services (like Sentry, Rollbar, or New Relic) to track and analyze errors
- Implement distributed tracing for complex systems
User-Friendly Error Messages
When displaying errors to users, consider the following:
- Use clear, non-technical language
- Provide actionable steps for resolution when possible
- Don't expose sensitive information or implementation details
- Consider the user's emotional state and use empathetic language
- For critical errors, provide alternative contact methods or support options
Good Example:
"We couldn't save your changes because the connection was lost. Please check your internet connection and try again. If the problem persists, please contact support."
Bad Example:
"Error 500: Internal server error occurred in module XYZ. Failed to execute database query: Connection timeout after 30s."
Retry Strategies
When implementing retry logic, consider these strategies:
- Exponential Backoff: Increase the delay between retries exponentially
- Jitter: Add randomness to retry intervals to prevent thundering herd problems
- Circuit Breaker Pattern: Stop retrying after a certain threshold to prevent cascading failures
- Retry Budgets: Limit the number of retries across your system
- Idempotency: Ensure operations can be safely retried without side effects
Conclusion
Effective error handling is a critical aspect of building robust API integrations. By implementing the best practices outlined in this guide, you can create applications that gracefully handle errors, provide a better user experience, and are easier to maintain and debug.
Key Takeaways
- Always check for and handle errors in API responses
- Implement retry logic for transient errors
- Use appropriate HTTP status codes and error formats
- Log errors with context for debugging
- Provide user-friendly error messages
- Consider different retry strategies for different types of errors