Effect 3.16+ introduces ExecutionPlan for declarative retry/fallback strategies across different providers. This is ideal for production systems needing high availability.
Requires Effect 3.16.0 or later. Check your version with pnpm list effect.
Basic Setup
import { ExecutionPlan, Schedule, Layer, Effect } from "effect"
import {
Provider,
HttpTransport,
getBalance
} from "voltaire-effect"
// Define fallback chain with different providers
const ProviderPlan = ExecutionPlan.make(
{
// Primary: Infura with 2 retries, 1 second apart
provide: Layer.provide(Provider, HttpTransport(process.env.INFURA_URL!)),
attempts: 2,
schedule: Schedule.spaced("1 second")
},
{
// Secondary: Alchemy with 3 retries, exponential backoff
provide: Layer.provide(Provider, HttpTransport(process.env.ALCHEMY_URL!)),
attempts: 3,
schedule: Schedule.exponential("500 millis")
},
{
// Fallback: Public RPC (single attempt)
provide: Layer.provide(Provider, HttpTransport("https://eth.llamarpc.com"))
}
)
// Apply to any effect requiring ProviderService
const robust = getBalance("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045").pipe(
Effect.withExecutionPlan(ProviderPlan)
)
await Effect.runPromise(robust)
How It Works
- First provider fails → Retries according to its schedule (up to
attempts times)
- All retries exhausted → Moves to next provider in the chain
- Success at any point → Returns result, skips remaining providers
- All providers fail → Returns error from final provider
Advanced Schedules
Combine Effect Schedule primitives for sophisticated retry logic:
import { Schedule } from "effect"
const ProviderPlan = ExecutionPlan.make(
{
provide: Layer.provide(Provider, HttpTransport(process.env.PRIMARY_RPC!)),
attempts: 5,
schedule: Schedule.exponential("200 millis").pipe(
Schedule.jittered, // Add randomness to prevent thundering herd
Schedule.either(Schedule.spaced("5 seconds")) // Cap at 5s max
)
},
{
provide: Layer.provide(Provider, HttpTransport(process.env.BACKUP_RPC!)),
// No attempts = single try, but schedule allows retries
schedule: Schedule.recurs(2) // 2 retries
}
)
Multiple Operations
Apply the same plan to multiple operations:
import { Effect } from "effect"
import { getBalance, getBlockNumber, getTransactionCount } from "voltaire-effect"
const address = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
const robustOperations = Effect.all({
balance: getBalance(address),
blockNumber: getBlockNumber(),
nonce: getTransactionCount(address)
}).pipe(
Effect.withExecutionPlan(ProviderPlan)
)
Benefits
| Feature | Description |
|---|
| Declarative | Define retry/fallback logic once, apply anywhere |
| Composable | Combine with Effect schedules for complex patterns |
| Type-safe | Full TypeScript inference for layers and errors |
| Observable | Works with Effect tracing and logging |
Comparison with FallbackTransport
FallbackTransport is transport-level, ExecutionPlan is effect-level:
// Transport-level: switches on any transport error
import { FallbackTransport } from "voltaire-effect"
const transport = FallbackTransport([
HttpTransport(process.env.PRIMARY_RPC!),
HttpTransport(process.env.BACKUP_RPC!)
])
// Effect-level: more control, works with any layer
const plan = ExecutionPlan.make(
{ provide: Layer.provide(Provider, HttpTransport(url1)), attempts: 3 },
{ provide: Layer.provide(Provider, HttpTransport(url2)) }
)
Use ExecutionPlan when you need:
- Different retry strategies per provider
- Fine-grained schedule control
- Fallback for non-transport layers (signers, formatters, etc.)
Next Steps