Guides

Error Handling

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

The Quote Gallery API uses standard HTTP status codes and returns consistent JSON error bodies. Every error response looks the same:

{
  "error": "Human-readable message",
  "status": 401
}

Error codes

Client errors (4xx)

StatusNameWhen it happens
400Bad RequestMalformed params — limit over 100, negative offset, invalid type
401UnauthorizedMissing or invalid X-API-Key header
403ForbiddenKey is valid but lacks permission for this resource
404Not FoundID doesn't exist or the resource isn't publicly approved
429Too Many RequestsHourly quota exhausted

Server errors (5xx)

StatusNameWhat to do
500Internal Server ErrorRetry. If it persists, contact support
502Bad GatewayTemporary upstream issue. Retry with backoff
503Service UnavailableMaintenance or high load. Retry after a short delay

Common mistakes

401 with a valid key — check for extra whitespace, truncation, or a Bearer prefix. The API expects the raw key in X-API-Key, not an Authorization header.

404 on a known ID — the resource may exist but not be publicly approved (drafts, rejected content). Only approved public content is visible via the API.

400 on paginationoffset must be >= 0 and limit must be between 1 and 100.

Handling errors by status code

Different errors call for different responses. 4xx errors (except 429) are your fault and shouldn't be retried. 5xx errors and 429s are retryable.

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:
      return { success: false, error: error.error, retryable: false }
    case 401:
      return { success: false, error: error.error, retryable: false }
    case 404:
      return { success: false, error: error.error, retryable: false }
    case 429: {
      const reset = response.headers.get('X-RateLimit-Reset')
      const retryAfter = reset
        ? Math.max((Number(reset) * 1000) - Date.now(), 1000)
        : 60000
      return { success: false, error: error.error, retryable: true, retryAfter }
    }
    default:
      return { success: false, error: error.error, retryable: true }
  }
}

Retry with exponential backoff

For 429 and 5xx responses, wait before retrying. Use X-RateLimit-Reset when available:

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()

    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
      await new Promise((resolve) => setTimeout(resolve, delay))
    }
  }

  throw new Error(`Failed after ${maxRetries + 1} attempts`)
}

A complete API client

This client handles timeouts, retries, and structured errors out of the box:

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(([k, v]) => {
      if (v !== undefined && v !== null) url.searchParams.set(k, String(v))
    })

    for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
      const controller = new AbortController()
      const timer = setTimeout(() => controller.abort(), this.timeout)

      try {
        const response = await fetch(url.toString(), {
          headers: { 'X-API-Key': this.apiKey },
          signal: controller.signal,
        })

        clearTimeout(timer)

        if (response.ok) return response.json()

        const error = await response.json()

        if (response.status !== 429 && response.status < 500) {
          throw new ApiError(error.error, response.status)
        }

        if (attempt < this.maxRetries) {
          const delay = this.#retryDelay(response, attempt)
          await new Promise((r) => setTimeout(r, delay))
          continue
        }

        throw new ApiError(error.error, response.status)
      } catch (err) {
        clearTimeout(timer)
        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)
      }
    }
  }

  #retryDelay(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)
  }

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