Guides

Error Handling

Learn about API error codes, response formats, and best practices for handling errors gracefully.

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
}
FieldTypeDescription
errorstringA human-readable description of the error
statusnumberThe HTTP status code (mirrors the response status)

Error Codes

Client Errors (4xx)

StatusNameDescription
400Bad RequestThe request was malformed or contains invalid parameters. Check your query parameters and request format.
401UnauthorizedAuthentication failed. Either no X-API-Key header was provided, or the provided API key is invalid or has been revoked.
403ForbiddenThe API key is valid but does not have permission to access the requested resource.
404Not FoundThe requested resource does not exist. This typically means the quote, author, or playlist ID is invalid or the resource is not publicly approved.
429Too Many RequestsYou have exceeded the rate limit for your current API tier. Wait for the rate limit window to reset before retrying.

Server Errors (5xx)

StatusNameDescription
500Internal Server ErrorAn unexpected error occurred on the server. If this persists, please contact support.
502Bad GatewayThe API server received an invalid response from an upstream service. This is usually temporary.
503Service UnavailableThe 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:

  • limit exceeds the maximum value of 100
  • offset is negative
  • language is 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-Key header
  • API key is invalid, expired, or revoked
  • API key was copied incorrectly (extra whitespace, truncated, etc.)
{
  "error": "Invalid API key",
  "status": 401
}
Double-check that your API key is being sent correctly. A common mistake is adding a 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:

HeaderDescription
X-RateLimit-LimitYour maximum requests per hour
X-RateLimit-RemainingRequests remaining (will be 0)
X-RateLimit-ResetUnix 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()
}

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)
  }
}
Copyright © 2026