Error Handling
spotify-effect uses Effect’s type system to represent errors as discriminated unions. Every API call can fail with a SpotifyRequestError, which is a union of specific error types.
Error Types
SpotifyHttpError
HTTP-level errors from the Spotify API when the failure is not classified more specifically.
import { SpotifyHttpError } from "@spotify-effect/core";
if (error._tag === "SpotifyHttpError") { console.log(error.status); // HTTP status code (e.g., 404) console.log(error.method); // 'GET', 'POST', etc. console.log(error.url); // Request URL console.log(error.apiMessage); // Error message from Spotify}Common causes:
400— Bad request (invalid parameters)401— Unauthorized (expired or invalid token)403— Forbidden (insufficient scopes)404— Not found (invalid resource ID)
SpotifyRateLimitError
Spotify’s rate limit was exceeded.
import { SpotifyRateLimitError } from "@spotify-effect/core";
if (error._tag === "SpotifyRateLimitError") { console.log(error.retryAfterSeconds); // Seconds to wait console.log(error.url);}SpotifyParseError
Failed to decode the API response (schema mismatch).
import { SpotifyParseError } from "@spotify-effect/core";
if (error._tag === "SpotifyParseError") { console.log(error.cause); // The decoding error console.log(error.url); // Request URL}SpotifyConfigurationError
Misconfiguration (missing credentials, invalid parameters).
import { SpotifyConfigurationError } from "@spotify-effect/core";
if (error._tag === "SpotifyConfigurationError") { console.log(error.message); // What went wrong}SpotifyTransportError
Network-level failures (connection issues, DNS errors).
import { SpotifyTransportError } from "@spotify-effect/core";
if (error._tag === "SpotifyTransportError") { console.log(error.cause); // The underlying error console.log(error.url); // Request URL}Handling Errors
Using Effect.catch
import { Effect } from "effect";import { SpotifyHttpError, SpotifyRateLimitError } from "@spotify-effect/core";
const program = Effect.gen(function* () { const browse = yield* Browse; return yield* browse.getNewReleases();}).pipe( Effect.catch("SpotifyHttpError", (error) => { if (error.status === 401) { return Effect.fail(new TokenExpiredError()); } return Effect.fail(error); }),);Using Effect.match
import { Effect } from "effect";
const result = yield * Effect.match({ onSuccess: (data) => data, onFailure: (error) => { switch (error._tag) { case "SpotifyHttpError": if (error.status === 404) return null; return error; case "SpotifyRateLimitError": console.log(`Rate limited. Retry after ${error.retryAfterSeconds}s`); return error; default: return error; } }, });Using Effect.exit
For cases where you need to handle success and failure differently:
import { Effect, Exit } from "effect";
const exit = yield * Effect.exit( Effect.gen(function* () { const search = yield* Search; return yield* search.search("Queen", ["artist"]); }), );
if (Exit.isSuccess(exit)) { console.log("Found artists:", exit.value.artists?.items);} else { console.log("Search failed:", exit.cause);}Retry Logic
Classifying retryable errors
Use isRetryableError to determine if an error should be retried:
import { isRetryableError } from "@spotify-effect/core";
if (isRetryableError(error)) { // Retry the request}isRetryableError returns true for:
SpotifyTransportError(network failures)SpotifyRateLimitErrorSpotifyHttpErrorwith status 429 or 5xx
You can also configure the shared request layer’s built-in retry behavior when constructing the Spotify layer:
import { makeSpotifyLayer } from "@spotify-effect/core";
const SpotifyLayer = makeSpotifyLayer({ clientId: process.env.SPOTIFY_CLIENT_ID!, clientSecret: process.env.SPOTIFY_CLIENT_SECRET!, retry: { maxRetries: 5, baseDelayMs: 250, maxDelayMs: 5_000, },});Set maxRetries: 0 to disable built-in retries and handle retry policy entirely in application code.
Implementing retry with backoff
import { Effect } from "effect";import { SpotifyRateLimitError, isRetryableError } from "@spotify-effect/core";
const withRetry = <A, E, R>( effect: Effect.Effect<A, E, R>, maxRetries = 3,): Effect.Effect<A, E, R> => Effect.retry(effect, { times: maxRetries, schedule: Schedule.exponential("1 second"), while: (error) => isRetryableError(error) && (error._tag !== "SpotifyRateLimitError" || error.retryAfterSeconds < 60), });Error Recovery Patterns
Fallback to cached data
const program = Effect.gen(function* () { const browse = yield* Browse; const newReleases = yield* Effect.either(browse.getNewReleases({ country: "US" }));
if (newReleases._tag === "Left") { return yield* getCachedReleases(); // Fallback }
yield* cacheReleases(newReleases.right); return newReleases.right;});Timeout with fallback
import { Effect, Duration } from "effect";
const withTimeout = <A, E, R>( effect: Effect.Effect<A, E, R>, timeout: Duration, fallback: A,): Effect.Effect<A, E, R> => Effect.match({ onSuccess: (a) => a, onFailure: (e) => e, })(Effect.race(effect, Effect.sleep(timeout).pipe(Effect.map(() => fallback))));Next steps
- Pagination — handle paginated result errors
- Authentication — handle auth-related errors