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)
- Read the
Retry-Afterheader - Wait the specified number of seconds
- Retry the request
For 500 (server error)
- Use exponential backoff
- Start with a 1 second delay
- Double the delay on each retry (1s, 2s, 4s, 8s...)
- Cap at a maximum delay (e.g., 60 seconds)
- 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');
}
}