Published on May 29, 2026

Your music integration will fail. Tokens expire mid-playlist sync. Streaming services return 429s during peak hours. Entire platforms go down for maintenance without warning. The difference between a production-ready music app and a demo is how your code handles these failures. This guide covers the error types you will hit, retry strategies that actually work, and degradation patterns that keep your users listening when things break.
Music API errors are not hypothetical. Every streaming service enforces rate limits, rotates auth tokens, and experiences downtime. A single unhandled 429 response can cascade into a failed playlist import, a broken playback queue, or a user staring at a loading spinner that never resolves. Solid error handling is the foundation of any music app that serves real users at scale.
Music APIs return a predictable set of error categories. Knowing what to expect lets you write targeted handlers instead of generic catch-all blocks.
OAuth tokens expire. Most streaming services issue access tokens that last between 30 minutes and one hour. If your app stores a token and reuses it without checking expiry, you will get 401 responses the moment the token lapses.
Revoked access is harder to detect. A user can disconnect your app from their streaming account at any time. The next API call returns a 401 or 403, but the error message varies by service. Some return "invalid_grant", others return "token_revoked", and some just give you a generic "Unauthorized".
// Typical auth error response
{
"error": {
"status": 401,
"message": "The access token expired"
}
}
The fix: always check token expiry before making a request, implement automatic token refresh, and handle revocation by prompting the user to re-authenticate.
Every streaming platform enforces rate limits, and they all do it differently. When you exceed the limit, you get a 429 Too Many Requests response. Most services include a Retry-After header telling you how long to wait.
// Rate limit response
HTTP/1.1 429 Too Many Requests
Retry-After: 30
{
"error": {
"status": 429,
"message": "API rate limit exceeded"
}
}
The dangerous part: rate limits often apply per-user AND per-app. You might stay under the per-user limit while your total app traffic triggers the per-app limit, affecting all your users at once. Read more about how rate limiting works across services.
Streaming services go down. Sometimes it is scheduled maintenance, sometimes it is an unplanned outage. Your code needs to handle both full outages (5xx errors, timeouts) and partial failures (some endpoints work, others do not).
Partial failures are the sneakiest. A service might return playlist metadata successfully but fail when you try to fetch track details. If your sync logic assumes all-or-nothing availability, a partial failure can corrupt your local data or leave users with half-synced playlists.
Not every error deserves a retry. A 401 from a revoked token will never succeed no matter how many times you repeat it. A 429 will succeed after the cooldown period. A 503 might resolve in seconds or hours. Your retry logic needs to distinguish between these cases.
Exponential backoff increases the wait time between retries, reducing pressure on the failing service. Adding jitter (randomness) prevents the "thundering herd" problem where all your retries hit the server at the same moment.
async function fetchWithRetry(url, options, maxRetries = 5) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const response = await fetch(url, options);
if (response.ok) {
return await response.json();
}
// Do not retry auth errors
if (response.status === 401 || response.status === 403) {
throw new Error(`Auth error ${response.status}: re-authentication required`);
}
// Retry rate limits with Retry-After header
if (response.status === 429) {
const retryAfter = parseInt(response.headers.get('Retry-After') || '5', 10);
await sleep(retryAfter * 1000);
continue;
}
// Retry server errors with exponential backoff + jitter
if (response.status >= 500) {
const baseDelay = Math.pow(2, attempt) * 1000;
const jitter = Math.random() * 1000;
await sleep(baseDelay + jitter);
continue;
}
throw new Error(`Request failed with status ${response.status}`);
} catch (err) {
if (err.message.includes('Auth error') || attempt === maxRetries - 1) {
throw err;
}
// Network errors: retry with backoff
const baseDelay = Math.pow(2, attempt) * 1000;
const jitter = Math.random() * 1000;
await sleep(baseDelay + jitter);
}
}
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
| Error Type | Status Code | Retry? | Strategy |
|---|---|---|---|
| Expired token | 401 | No | Refresh the token, then retry once |
| Revoked access | 401 / 403 | No | Prompt user to re-authenticate |
| Rate limited | 429 | Yes | Wait for Retry-After, then retry |
| Server error | 500 | Yes | Exponential backoff with jitter |
| Service unavailable | 503 | Yes | Exponential backoff, max 3 retries |
| Bad request | 400 | No | Fix the request payload |
| Not found | 404 | No | Resource does not exist |
| Network timeout | N/A | Yes | Exponential backoff, check connectivity |
The rule: retry transient errors (429, 5xx, network issues). Fail fast on client errors (400, 401, 403, 404). Retrying a bad request wastes time and server resources.
If your app connects to multiple streaming services, you are managing multiple rate limit policies at once. Each service counts requests differently, returns rate limit headers in different formats, and enforces limits at different scopes.
| Policy | Service A | Service B | Service C | Service D | Service E |
|---|---|---|---|---|---|
| Limit scope | Per-app + per-user | Per-developer token | Per-project daily quota | Per-app | Per-app |
| Rate limit header | Retry-After (seconds) | None (fixed cooldown) | Quota usage in response | Retry-After (seconds) | X-RateLimit-Remaining |
| Typical limit | ~180 requests/min | ~20 requests/sec | 10,000 units/day | Varies by endpoint | ~50 requests/5sec |
| Burst handling | Allows short bursts | Strict token bucket | Counts all calls equally | Allows moderate bursts | Sliding window |
| Pagination impact | Cursor-based (1 call per page) | Offset-based (1 call per page) | Each page costs 1 quota unit | Offset-based | Offset-based |
Building a unified rate limit layer across five services means tracking five different counter formats, five different cooldown mechanisms, and five different scopes. When you batch-sync a user's library across platforms, one service hitting its limit should not block operations on the others.
MusicAPI handles this complexity for you. The unified rate limiting layer normalizes rate limit behavior across all supported streaming services. You make requests through a single API, and MusicAPI manages per-service throttling, queuing, and retry logic behind the scenes. No need to build and maintain five separate rate limit trackers.
When a streaming service fails, your app does not have to fail with it. Graceful degradation means your users see reduced functionality instead of error screens.
Cache aggressively. Playlist metadata, track listings, and user profile data change infrequently. If a service is down, serve the last known good data with a timestamp showing when it was last refreshed.
async function getPlaylistTracks(playlistId, connectionToken) {
const cacheKey = `playlist:${playlistId}:tracks`;
try {
const response = await fetchWithRetry(
`https://api.musicapi.com/playlist/${playlistId}/tracks`,
{ headers: { 'Authorization': `Bearer ${connectionToken}` } }
);
// Update cache with fresh data
cache.set(cacheKey, {
data: response.data,
fetchedAt: new Date().toISOString()
});
return { data: response.data, fromCache: false };
} catch (err) {
// Fall back to cached data if available
const cached = cache.get(cacheKey);
if (cached) {
return { data: cached.data, fromCache: true, cachedAt: cached.fetchedAt };
}
throw err; // No cache available, propagate the error
}
}
When serving cached data, always tell the user. A small "last updated 2 hours ago" label builds more trust than silently serving stale results.
A circuit breaker prevents your app from hammering a service that is already down. After a threshold of consecutive failures, the circuit "opens" and all requests to that service return immediately with a fallback response. After a cooldown period, the circuit enters "half-open" state and lets one test request through.
class CircuitBreaker {
constructor(failureThreshold = 5, cooldownMs = 30000) {
this.failureCount = 0;
this.failureThreshold = failureThreshold;
this.cooldownMs = cooldownMs;
this.state = 'closed'; // closed = normal, open = blocking, half-open = testing
this.lastFailureTime = null;
}
async execute(fn, fallback) {
if (this.state === 'open') {
const elapsed = Date.now() - this.lastFailureTime;
if (elapsed < this.cooldownMs) {
return fallback();
}
this.state = 'half-open';
}
try {
const result = await fn();
this.onSuccess();
return result;
} catch (err) {
this.onFailure();
if (this.state === 'open') {
return fallback();
}
throw err;
}
}
onSuccess() {
this.failureCount = 0;
this.state = 'closed';
}
onFailure() {
this.failureCount++;
this.lastFailureTime = Date.now();
if (this.failureCount >= this.failureThreshold) {
this.state = 'open';
}
}
}
// Usage per service
const spotifyCircuit = new CircuitBreaker(5, 60000);
const tracks = await spotifyCircuit.execute(
() => fetchPlaylistTracks(playlistId),
() => getCachedPlaylistTracks(playlistId)
);
Use one circuit breaker per service. If one platform is experiencing issues, your app can continue operating normally for users on other streaming services. This is where a unified API like MusicAPI helps: you write one integration, and the platform handles per-service health monitoring and failover internally.
Use exponential backoff with jitter. Start with a 1-second delay, double it on each retry, and add a random offset between 0 and 1 second. Always respect the Retry-After header when the service provides one. Cap your maximum retry count at 3 to 5 attempts to avoid holding connections open indefinitely.
Store the token expiry timestamp alongside the access token. Before each API request, check if the token expires within the next 60 seconds. If it does, use the refresh token to get a new access token. If the refresh fails (the user revoked access), redirect them to the authentication flow to reconnect their account.
No. A 401 means your credentials are invalid. Retrying the same request will produce the same result. Instead, attempt a token refresh. If the refresh succeeds, retry the original request once with the new token. If the refresh fails, the user needs to re-authorize your app.
Track rate limits independently for each service. Use separate request queues with per-service throttling. Implement a token bucket or sliding window counter for each platform. If one service is rate-limited, continue processing requests for the others. MusicAPI's unified rate limiting handles this automatically across all supported services.
A circuit breaker monitors consecutive failures to a service. After a threshold (typically 5 failures), it stops sending requests and returns cached or fallback data instead. After a cooldown period, it sends a single test request. If that succeeds, normal traffic resumes. Use circuit breakers when your app depends on external services that can experience prolonged outages.
Cache all data you fetch from streaming services with a timestamp. When a request fails and retries are exhausted, serve cached data with a "last updated" indicator. Use circuit breakers to detect service outages early. Show users a status indicator for each connected service so they know which platform is experiencing issues.
Handle 400 (bad request, fix your payload), 401 (auth expired or revoked), 403 (insufficient permissions), 404 (resource deleted or unavailable), 429 (rate limited, respect Retry-After), 500/502/503 (server issues, retry with backoff), and network timeouts. Each requires a different response strategy. See the retry table above for the recommended approach per code.
Reliable error handling is not optional for production music apps. Between expired tokens, inconsistent rate limits, and surprise outages, every integration point is a potential failure. The patterns in this guide (exponential backoff, circuit breakers, cache fallbacks) give you the building blocks.
Ready to skip months of OAuth and SDK work? Start your free MusicAPI trial and connect 10+ streaming services with one unified API. MusicAPI handles authentication, rate limiting, and error recovery across every supported platform, so you can focus on building features instead of debugging service-specific edge cases.