Error Handling
Handle API errors gracefully with proper retry logic and user feedback. Learn the error codes, when to retry, and how to build resilient integrations.
Error Response Format
All API errors return a consistent JSON structure:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Validation failed",
"details": [
{ "field": "price", "reason": "Must be a positive number" },
{ "field": "slug", "reason": "Already exists" }
]
}
}| Field | Type | Description |
|---|---|---|
| code | string | Machine-readable error code |
| message | string | Human-readable description |
| details | array? | Field-level errors (for validation) |
Error Codes
Client Errors (4xx)
| Status | Code | Description | Retry? |
|---|---|---|---|
| 400 | BAD_REQUEST | Malformed request | ❌ Fix request |
| 400 | VALIDATION_ERROR | Field validation failed | ❌ Fix fields |
| 401 | UNAUTHORIZED | Missing/invalid API key | ❌ Check key |
| 403 | FORBIDDEN | Insufficient permissions | ❌ Check scopes |
| 404 | NOT_FOUND | Resource doesn't exist | ❌ Check ID |
| 409 | CONFLICT | Duplicate data | ❌ Resolve conflict |
| 429 | RATE_LIMITED | Too many requests | ✅ With backoff |
Server Errors (5xx)
| Status | Code | Description | Retry? |
|---|---|---|---|
| 500 | INTERNAL_ERROR | Server error | ✅ With backoff |
| 503 | SERVICE_UNAVAILABLE | Temporarily unavailable | ✅ With backoff |
Retry Strategy
Use exponential backoff with jitter for retryable errors:
async function fetchWithRetry(url, options, maxRetries = 3) {
let lastError;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const response = await fetch(url, options);
// Don't retry client errors (except 429)
if (response.status >= 400 && response.status < 500 && response.status !== 429) {
const error = await response.json();
throw new APIError(response.status, error);
}
// Retry on 429 or 5xx
if (response.status === 429 || response.status >= 500) {
throw new RetryableError(response.status);
}
return response;
} catch (error) {
lastError = error;
if (error instanceof APIError) throw error;
if (attempt < maxRetries - 1) {
// Exponential backoff: 1s, 2s, 4s + jitter
const delay = Math.pow(2, attempt) * 1000;
const jitter = Math.random() * 1000;
await sleep(delay + jitter);
}
}
}
throw lastError;
}Retry Decision Chart
| Status | Retry? | Action |
|---|---|---|
| 400, 401, 403, 404, 409 | ❌ | Fix the issue first |
| 429 | ✅ | Retry with backoff |
| 500, 502, 503, 504 | ✅ | Retry with exponential backoff |
| Network error | ✅ | Retry with exponential backoff |
Handling Validation Errors
Validation errors include field-level details:
async function createProduct(data) {
const response = await fetch('/stores/{storeId}/products', {
method: 'POST',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
if (!response.ok) {
const { error } = await response.json();
if (error.code === 'VALIDATION_ERROR' && error.details) {
// Map errors to form fields
const fieldErrors = {};
for (const detail of error.details) {
fieldErrors[detail.field] = detail.reason;
}
return { success: false, fieldErrors };
}
throw new Error(error.message);
}
return { success: true, data: await response.json() };
}Graceful Degradation
Build resilient UIs that handle failures gracefully:
function ProductList() {
const [products, setProducts] = useState([]);
const [error, setError] = useState(null);
useEffect(() => {
async function loadProducts() {
try {
const response = await fetchWithRetry('/stores/{storeId}/products');
const data = await response.json();
setProducts(data.items);
// Cache for fallback
localStorage.setItem('products_cache', JSON.stringify(data.items));
} catch (err) {
setError(err);
// Load from cache
const cached = localStorage.getItem('products_cache');
if (cached) setProducts(JSON.parse(cached));
}
}
loadProducts();
}, []);
if (error && products.length === 0) {
return <ErrorState message="Unable to load products" />;
}
return (
<>
{error && <Banner>Showing cached data</Banner>}
<ProductGrid products={products} />
</>
);
}Logging Errors
Log errors with context for debugging:
async function apiRequest(endpoint, options) {
const requestId = generateRequestId();
try {
const response = await fetch(endpoint, options);
if (!response.ok) {
const error = await response.json();
console.error('API Error', {
requestId,
endpoint,
status: response.status,
code: error.error?.code,
message: error.error?.message,
});
throw new APIError(response.status, error);
}
return response;
} catch (err) {
console.error('Network Error', { requestId, endpoint, error: err.message });
throw err;
}
}Best Practices
- Check error codes, not just status — Use
error.codefor precise handling - Implement exponential backoff — Add jitter to prevent thundering herd
- Show user-friendly messages — Don't expose raw API errors
- Log errors with context — Include request ID, endpoint, and timing
- Cache for graceful degradation — Show stale data rather than errors
Related Guides
- Authentication — Handle 401/403 errors
- Performance — Avoid rate limits with caching