Skip to main content
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

  1. First provider fails → Retries according to its schedule (up to attempts times)
  2. All retries exhausted → Moves to next provider in the chain
  3. Success at any point → Returns result, skips remaining providers
  4. 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

FeatureDescription
DeclarativeDefine retry/fallback logic once, apply anywhere
ComposableCombine with Effect schedules for complex patterns
Type-safeFull TypeScript inference for layers and errors
ObservableWorks 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