Skip to content

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)
  • SpotifyRateLimitError
  • SpotifyHttpError with 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