Error Handling
Error Handling
The Quote Gallery API uses conventional HTTP status codes and returns consistent JSON error responses. This guide covers all possible error codes and best practices for handling them in your application.
Error Response Format
All error responses follow the same JSON structure:
{
"error": "Human-readable error message",
"status": 401
}
| Field | Type | Description |
|---|---|---|
error | string | A human-readable description of the error |
status | number | The HTTP status code (mirrors the response status) |
Error Codes
Client Errors (4xx)
| Status | Name | Description |
|---|---|---|
400 | Bad Request | The request was malformed or contains invalid parameters. Check your query parameters and request format. |
401 | Unauthorized | Authentication failed. Either no X-API-Key header was provided, or the provided API key is invalid or has been revoked. |
403 | Forbidden | The API key is valid but does not have permission to access the requested resource. |
404 | Not Found | The requested resource does not exist. This typically means the quote, author, or playlist ID is invalid or the resource is not publicly approved. |
429 | Too Many Requests | You have exceeded the rate limit for your current API tier. Wait for the rate limit window to reset before retrying. |
Server Errors (5xx)
| Status | Name | Description |
|---|---|---|
500 | Internal Server Error | An unexpected error occurred on the server. If this persists, please contact support. |
502 | Bad Gateway | The API server received an invalid response from an upstream service. This is usually temporary. |
503 | Service Unavailable | The API is temporarily unavailable due to maintenance or high load. Retry after a short delay. |
Detailed Error Scenarios
400 — Bad Request
Returned when the request contains invalid parameters.
Common causes:
limitexceeds the maximum value of100offsetis negativelanguageis not a valid language code- Query parameter has an invalid type (e.g., passing a string where a number is expected)
{
"error": "Invalid limit parameter: must be between 1 and 100",
"status": 400
}
401 — Unauthorized
Returned when authentication fails.
Common causes:
- Missing
X-API-Keyheader - API key is invalid, expired, or revoked
- API key was copied incorrectly (extra whitespace, truncated, etc.)
{
"error": "Invalid API key",
"status": 401
}
Bearer prefix — the Quote Gallery API expects the raw key in the X-API-Key header, not an Authorization Bearer token.404 — Not Found
Returned when the requested resource doesn't exist.
Common causes:
- The quote, author, or playlist ID is incorrect
- The resource exists but is not publicly approved (e.g., a draft or rejected quote)
- The resource was deleted
{
"error": "Quote not found",
"status": 404
}
429 — Too Many Requests
Returned when you exceed your rate limit.
{
"error": "Rate limit exceeded",
"status": 429
}
The response includes headers to help you handle this gracefully:
| Header | Description |
|---|---|
X-RateLimit-Limit | Your maximum requests per hour |
X-RateLimit-Remaining | Requests remaining (will be 0) |
X-RateLimit-Reset | Unix timestamp when the limit resets |
See the Rate Limits & Pricing guide for strategies on handling rate limits.
Best Practices
1. Always Check the Response Status
Never assume a request will succeed. Always check the HTTP status code before processing the response body.
async function fetchQuotes(apiKey) {
const response = await fetch('https://quotegallery.nl/api/v1/quotes', {
headers: { 'X-API-Key': apiKey },
})
if (!response.ok) {
const error = await response.json()
throw new Error(`API Error ${error.status}: ${error.error}`)
}
return response.json()
}
import requests
def fetch_quotes(api_key):
response = requests.get(
'https://quotegallery.nl/api/v1/quotes',
headers={'X-API-Key': api_key}
)
if not response.ok:
error = response.json()
raise Exception(f'API Error {error["status"]}: {error["error"]}')
return response.json()
2. Handle Errors by Status Code
Different errors require different handling strategies:
async function safeApiFetch(url, apiKey) {
const response = await fetch(url, {
headers: { 'X-API-Key': apiKey },
})
if (response.ok) {
return { success: true, data: await response.json() }
}
const error = await response.json()
switch (response.status) {
case 400:
// Bad request — fix the parameters and don't retry
console.error('Invalid request:', error.error)
return { success: false, error: error.error, retryable: false }
case 401:
// Auth failure — check API key, don't retry with the same key
console.error('Authentication failed:', error.error)
return { success: false, error: error.error, retryable: false }
case 404:
// Resource not found — the ID might be wrong
console.error('Resource not found:', error.error)
return { success: false, error: error.error, retryable: false }
case 429:
// Rate limited — retry after waiting
const resetTime = response.headers.get('X-RateLimit-Reset')
const retryAfter = resetTime
? Math.max((Number(resetTime) * 1000) - Date.now(), 1000)
: 60000
console.warn(`Rate limited. Retry after ${Math.ceil(retryAfter / 1000)}s`)
return { success: false, error: error.error, retryable: true, retryAfter }
default:
// Server error — retry with backoff
console.error(`Server error ${response.status}:`, error.error)
return { success: false, error: error.error, retryable: true }
}
}
3. Implement Retry with Exponential Backoff
For retryable errors (429, 5xx), use exponential backoff to avoid overwhelming the API:
async function fetchWithBackoff(url, apiKey, maxRetries = 3) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const response = await fetch(url, {
headers: { 'X-API-Key': apiKey },
})
if (response.ok) {
return response.json()
}
// Only retry on rate limits and server errors
if (response.status !== 429 && response.status < 500) {
const error = await response.json()
throw new Error(`API Error ${response.status}: ${error.error}`)
}
if (attempt < maxRetries) {
const baseDelay = response.status === 429 ? 5000 : 1000
const delay = baseDelay * Math.pow(2, attempt) + Math.random() * 1000
console.log(`Attempt ${attempt + 1} failed. Retrying in ${Math.ceil(delay / 1000)}s...`)
await new Promise((resolve) => setTimeout(resolve, delay))
}
}
throw new Error(`Failed after ${maxRetries + 1} attempts`)
}
4. Log Errors for Debugging
Maintain structured logs that capture enough context for debugging:
function logApiError(endpoint, statusCode, errorMessage, context = {}) {
const logEntry = {
timestamp: new Date().toISOString(),
service: 'quote-gallery-api',
endpoint,
statusCode,
error: errorMessage,
...context,
}
if (statusCode >= 500) {
console.error('API Server Error:', JSON.stringify(logEntry))
} else if (statusCode === 429) {
console.warn('API Rate Limit:', JSON.stringify(logEntry))
} else {
console.info('API Client Error:', JSON.stringify(logEntry))
}
}
5. Provide User-Friendly Messages
Don't expose raw API errors to your end users. Map them to friendly messages:
const USER_FRIENDLY_MESSAGES = {
400: 'Something went wrong with the request. Please try again.',
401: 'Unable to connect to the quotes service. Please check your configuration.',
404: 'The requested quote or author could not be found.',
429: 'We\'re making too many requests right now. Please wait a moment and try again.',
500: 'The quotes service is experiencing issues. Please try again later.',
503: 'The quotes service is temporarily unavailable. Please try again in a few minutes.',
}
function getUserMessage(statusCode) {
return USER_FRIENDLY_MESSAGES[statusCode]
?? 'An unexpected error occurred. Please try again later.'
}
Building a Robust API Client
Here's a complete example of a resilient API client that incorporates all the best practices above:
class QuoteGalleryClient {
constructor(apiKey, options = {}) {
this.apiKey = apiKey
this.baseUrl = options.baseUrl ?? 'https://quotegallery.nl/api/v1'
this.maxRetries = options.maxRetries ?? 3
this.timeout = options.timeout ?? 10000
}
async request(endpoint, params = {}) {
const url = new URL(`${this.baseUrl}${endpoint}`)
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
url.searchParams.set(key, String(value))
}
})
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), this.timeout)
try {
const response = await fetch(url.toString(), {
headers: { 'X-API-Key': this.apiKey },
signal: controller.signal,
})
clearTimeout(timeoutId)
if (response.ok) {
return response.json()
}
const error = await response.json()
// Don't retry client errors (except rate limits)
if (response.status !== 429 && response.status < 500) {
throw new ApiError(error.error, response.status)
}
// Retry rate limits and server errors
if (attempt < this.maxRetries) {
const delay = this.getRetryDelay(response, attempt)
await new Promise((resolve) => setTimeout(resolve, delay))
continue
}
throw new ApiError(error.error, response.status)
} catch (err) {
clearTimeout(timeoutId)
if (err instanceof ApiError) throw err
if (err.name === 'AbortError') {
throw new ApiError('Request timed out', 408)
}
throw new ApiError(`Network error: ${err.message}`, 0)
}
}
}
getRetryDelay(response, attempt) {
if (response.status === 429) {
const reset = response.headers.get('X-RateLimit-Reset')
if (reset) {
return Math.max((Number(reset) * 1000) - Date.now(), 1000)
}
}
return Math.min(1000 * Math.pow(2, attempt) + Math.random() * 1000, 30000)
}
// Convenience methods
getQuotes(params) { return this.request('/quotes', params) }
getRandomQuote(params) { return this.request('/quotes/random', params) }
getQuote(id, params) { return this.request(`/quotes/${id}`, params) }
getAuthors(params) { return this.request('/authors', params) }
getAuthor(id, params) { return this.request(`/authors/${id}`, params) }
getPlaylists(params) { return this.request('/playlists', params) }
getPlaylist(id, params) { return this.request(`/playlists/${id}`, params) }
getCategories() { return this.request('/categories') }
}
class ApiError extends Error {
constructor(message, statusCode) {
super(message)
this.name = 'ApiError'
this.statusCode = statusCode
}
}
Usage
const client = new QuoteGalleryClient(process.env.QUOTE_GALLERY_API_KEY)
try {
const { data: quotes } = await client.getQuotes({
language: 'en',
categories: 'wisdom',
limit: 10,
})
quotes.forEach((q) => console.log(`"${q.text}" — ${q.author.name}`))
} catch (err) {
if (err instanceof ApiError) {
console.error(`API Error ${err.statusCode}: ${err.message}`)
} else {
console.error('Unexpected error:', err)
}
}