| title | Retry Operations Based on Specific Errors | |||||
|---|---|---|---|---|---|---|
| id | retry-based-on-specific-errors | |||||
| skillLevel | intermediate | |||||
| applicationPatternId | error-management | |||||
| summary | Use Effect.retry and predicate functions to selectively retry an operation only when specific, recoverable errors occur. | |||||
| tags |
|
|||||
| rule |
|
|||||
| related |
|
|||||
| author | effect_website | |||||
| lessonOrder | 11 |
To selectively retry an operation, use Effect.retry with a Schedule that includes a predicate. The most common way is to use Schedule.whileInput((error) => ...), which will continue retrying only as long as the predicate returns true for the error that occurred.
Not all errors are created equal. Retrying on a permanent error like "permission denied" or "not found" is pointless and can hide underlying issues. You only want to retry on transient, recoverable errors, such as network timeouts or "server busy" responses.
By adding a predicate to your retry schedule, you gain fine-grained control over the retry logic. This allows you to build much more intelligent and efficient error handling systems that react appropriately to different failure modes. This is a common requirement for building robust clients for external APIs.
This example simulates an API client that can fail with different, specific error types. The retry policy is configured to only retry on ServerBusyError and give up immediately on NotFoundError.
import { Data, Effect, Schedule } from "effect";
// Define specific, tagged errors for our API client
class ServerBusyError extends Data.TaggedError("ServerBusyError") {}
class NotFoundError extends Data.TaggedError("NotFoundError") {}
let attemptCount = 0;
// A flaky API call that can fail in different ways
const flakyApiCall = Effect.try({
try: () => {
attemptCount++;
const random = Math.random();
if (attemptCount <= 2) {
// First two attempts fail with ServerBusyError (retryable)
console.log(
`Attempt ${attemptCount}: API call failed - Server is busy. Retrying...`
);
throw new ServerBusyError();
}
// Third attempt succeeds
console.log(`Attempt ${attemptCount}: API call succeeded!`);
return { data: "success", attempt: attemptCount };
},
catch: (e) => e as ServerBusyError | NotFoundError,
});
// A predicate that returns true only for the error we want to retry
const isRetryableError = (e: ServerBusyError | NotFoundError) =>
e._tag === "ServerBusyError";
// A policy that retries 3 times, but only if the error is retryable
const selectiveRetryPolicy = Schedule.recurs(3).pipe(
Schedule.whileInput(isRetryableError),
Schedule.addDelay(() => "100 millis")
);
const program = Effect.gen(function* () {
yield* Effect.logInfo("=== Retry Based on Specific Errors Demo ===");
try {
const result = yield* flakyApiCall.pipe(Effect.retry(selectiveRetryPolicy));
yield* Effect.logInfo(`Success: ${JSON.stringify(result)}`);
return result;
} catch (error) {
yield* Effect.logInfo("This won't be reached due to Effect error handling");
return null;
}
}).pipe(
Effect.catchAll((error) =>
Effect.gen(function* () {
if (error instanceof NotFoundError) {
yield* Effect.logInfo("Failed with NotFoundError - not retrying");
} else if (error instanceof ServerBusyError) {
yield* Effect.logInfo("Failed with ServerBusyError after all retries");
} else {
yield* Effect.logInfo(`Failed with unexpected error: ${error}`);
}
return null;
})
)
);
// Also demonstrate a case where NotFoundError is not retried
const demonstrateNotFound = Effect.gen(function* () {
yield* Effect.logInfo("\n=== Demonstrating Non-Retryable Error ===");
const alwaysNotFound = Effect.fail(new NotFoundError());
const result = yield* alwaysNotFound.pipe(
Effect.retry(selectiveRetryPolicy),
Effect.catchAll((error) =>
Effect.gen(function* () {
yield* Effect.logInfo(`NotFoundError was not retried: ${error._tag}`);
return null;
})
)
);
return result;
});
Effect.runPromise(program.pipe(Effect.flatMap(() => demonstrateNotFound)));Using a generic Effect.retry that retries on all errors. This can lead to wasted resources and obscure permanent issues.
import { Effect, Schedule } from "effect";
import { flakyApiCall } from "./somewhere"; // From previous example
// ❌ WRONG: This policy will retry even if the API returns a 404 Not Found.
// This wastes time and network requests on an error that will never succeed.
const blindRetryPolicy = Schedule.recurs(3);
const program = flakyApiCall.pipe(Effect.retry(blindRetryPolicy));