Error handling

The API uses standard HTTP status codes. Most error responses are JSON, and a few are plain text. This page covers what each one means and how to respond.

Status codes

Code Meaning Action
200 Success Parse the response body
401 Unauthorized Check your API key, or the request was a cache miss
404 Not found Lyrics not available for this track
422 Unprocessable Neither song nor artist was sent
429 Too many requests Rate limited. Check the Retry-After header
500 Server error Upstream failure. Retry with backoff
503 Service unavailable The server is serving only cached lyrics and the query is not cached. Retry later

Error response format

JSON error responses always have error. Some also have message:

{
  "error": "Short error description",
  "message": "Longer explanation with suggested action"
}

Two errors are plain text (Content-Type: text/plain) instead: 422, and the 429 sent when both rate limit tiers are exhausted. Check the Content-Type header before you parse the body as JSON.

Common errors

Missing parameters (422)

Song name or artist name not provided

Plain text. Sent only when both s (song) and a (artist) are missing. Always send all four parameters anyway: the cache key includes album and duration, so a request without them often misses the cached entry and, without an API key, returns 401.

Lyrics not found (404)

{
  "error": "Lyrics not available for this track"
}

No lyrics exist for this song. The result is cached for 7 days (negative cache). Check X-Cache-Status: NEGATIVE_HIT to know if this is a cached result.

API key required (401)

{
  "error": "API key required",
  "message": "Uncached queries require a valid API key via X-API-Key header"
}

The requested lyrics are not cached and the server requires an API key for fresh fetches. First check that the request sends album and duration: the cache key includes both, so a request without them often misses an entry that is cached. Keys aren't being issued right now (see Authentication). If you have a key, pass it via X-API-Key. Otherwise, play the song once through the Better Lyrics extension to prime the cache, then retry.

Rate limited (429)

When both rate limit tiers are exhausted, the body is plain text and Retry-After is 1:

Too Many Requests

When only the cached tier is left and the query is not cached, the body is JSON and Retry-After is 60:

{
  "error": "Rate limit exceeded. This request requires cached data, but no cache is available for this query.",
  "message": "Please try again later or reduce your request rate."
}

Check the Retry-After header and X-RateLimit-Type to see which case you hit. See Rate limiting.

Circuit breaker

The API puts a circuit breaker in front of the upstream provider. When the upstream fails repeatedly, the breaker "opens" so the failures do not cascade.

Circuit breaker states

State Behavior
CLOSED Normal operation. Requests go through
OPEN Tripped. Requests fail fast, and stale cache is served if available
HALF-OPEN Testing recovery. A limited number of requests go through

While the breaker is open

  • Cached responses are still served (both positive and stale cache)
  • Fresh fetch requests fail fast instead of timing out
  • Every so often the breaker lets a test request through to check whether upstream has recovered

Retry strategies

For 429 (rate limited)

  1. Read the Retry-After header
  2. Wait the specified number of seconds
  3. Retry the request

For 500 (server error)

  1. Use exponential backoff
  2. Start with a 1 second delay
  3. Double the delay on each retry (1s, 2s, 4s, 8s...)
  4. Cap at a maximum delay (e.g., 60 seconds)
  5. Give up after a maximum number of retries (e.g., 5)

For 404 (not found)

Do not retry immediately. The negative result is cached for 7 days. If you believe lyrics should exist, use the /revalidate endpoint with an API key to force a fresh lookup.

Example error handling (JavaScript)

async function fetchLyrics(song, artist, album, duration) {
  const url = `https://api.betterlyrics.org/getLyrics?s=${encodeURIComponent(song)}&a=${encodeURIComponent(artist)}&al=${encodeURIComponent(album)}&d=${duration}`;

  const response = await fetch(url);

  if (response.ok) {
    return await response.json();
  }

  const isJson = response.headers.get('Content-Type')?.includes('application/json');
  const error = isJson ? await response.json() : { error: await response.text() };

  switch (response.status) {
    case 404:
      // Lyrics not available - cache this locally
      return { notFound: true, cached: response.headers.get('X-Cache-Status') === 'NEGATIVE_HIT' };

    case 429:
      // Rate limited - get retry time
      const retryAfter = parseInt(response.headers.get('Retry-After') || '60');
      throw new Error(`Rate limited. Retry after ${retryAfter} seconds.`);

    case 401:
      // Auth required for cache miss
      throw new Error('API key required for uncached queries');

    default:
      throw new Error(error.message || error.error || 'Unknown error');
  }
}