Core Concepts
Effect is a TypeScript library for building reliable software. Three things matter the most in Voltaire-Effect:
- Effect - A description of a computation that may fail or require dependencies. Effects are similar to promises but more strongly typed and lazily executed.
- Schema - Runtime validation that produces typed values. Similar to Zod but for Effect.
- Services - Dependency injection without magic. Similar to React Context. In Voltaire we use this to inject providers, signers, chains and contracts into your programs.
Effect Type
Effect<Success, Error, Requirements>
Success - What the computation returns on success
Error - What errors it can produce (typed, not unknown)
Requirements - What services it needs to run
import * as Effect from 'effect/Effect'
// A computation that returns string, fails with Error, needs nothing
const program: Effect.Effect<string, Error, never> = Effect.succeed("hello")
// Run it
const result = await Effect.runPromise(program)
As a best practice you should only run runPromise or runSync at the very top of your program. It is an antipattern to use it within your program as it breaks Effects composability.
Generator Syntax
Effect.gen provides async/await-like syntax for chaining effects:
import * as Effect from 'effect/Effect'
const program = Effect.gen(function* () {
const a = yield* Effect.succeed(1)
const b = yield* Effect.succeed(2)
return a + b
})
yield* unwraps the success value. Errors short-circuit automatically.
Schema
Schema validates unknown input and returns typed output:
import * as S from 'effect/Schema'
const User = S.Struct({
name: S.String,
age: S.Number
})
// Sync decode - throws on failure
const user = S.decodeSync(User)({ name: "Alice", age: 30 })
// Effect decode - returns Effect with typed ParseError
const userEffect = S.decode(User)({ name: "Alice", age: 30 })
voltaire-effect schemas decode directly to Voltaire branded types:
import * as Address from 'voltaire-effect/primitives/Address'
import * as A from '@tevm/voltaire/Address'
import * as S from 'effect/Schema'
const addr = S.decodeSync(Address.Hex)('0x742d35Cc...')
// addr is AddressType (branded Uint8Array), not string
// Use with Voltaire functions
A.toHex(addr) // "0x742d35cc..."
A.isZero(addr) // false
Services
Services are typed dependencies. Define a service, implement it, provide it:
import * as Effect from 'effect/Effect'
import * as Context from 'effect/Context'
// 1. Define the service interface
class Logger extends Context.Tag("Logger")<
Logger,
{ log: (msg: string) => Effect.Effect<void> }
>() {}
// 2. Use the service in a program
const program = Effect.gen(function* () {
const logger = yield* Logger
yield* logger.log("Hello")
})
// 3. Provide the implementation
const LoggerLive = Effect.provideService(
program,
Logger,
{ log: (msg) => Effect.sync(() => console.log(msg)) }
)
voltaire-effect uses this for crypto:
import { KeccakService, KeccakLive } from 'voltaire-effect/crypto/Keccak256'
const program = Effect.gen(function* () {
const keccak = yield* KeccakService
return yield* keccak.hash(data)
}).pipe(Effect.provide(KeccakLive))
Error Handling
Errors are values, not exceptions. Handle them with catchTag:
import * as Effect from 'effect/Effect'
import * as S from 'effect/Schema'
S.decode(Address.Hex)(input).pipe(
Effect.catchTag("ParseError", (e) =>
Effect.succeed(Address.zero())
)
)
Pipe
pipe chains transformations left-to-right:
import { pipe } from 'effect'
const result = pipe(
5,
(x) => x + 1,
(x) => x * 2
)
// 12
Use it to compose Effects:
pipe(
S.decode(Address.Hex)(input),
Effect.map(addr => addr.toHex()),
Effect.catchTag("ParseError", () => Effect.succeed("0x0")),
Effect.provide(KeccakLive)
)
Duration & Retry
Effect uses human-readable duration strings:
import * as Duration from 'effect/Duration'
import * as Schedule from 'effect/Schedule'
// Duration strings
"30 seconds"
"5 minutes"
Duration.seconds(30)
// Retry with exponential backoff
Schedule.exponential("500 millis").pipe(
Schedule.jittered,
Schedule.recurs(5)
)
Per-Request Configuration
Use FiberRef-based helpers for scoped overrides:
import { getBalance, getBlock, getBlockNumber, withTimeout, withRetrySchedule, withoutCache } from 'voltaire-effect'
import * as Schedule from 'effect/Schedule'
// Override timeout for single request
getBalance(addr).pipe(withTimeout("5 seconds"))
// Custom retry schedule
getBlock({ blockNumber }).pipe(
withRetrySchedule(Schedule.recurs(1))
)
// Disable caching for fresh data
getBlockNumber().pipe(withoutCache)
Running Effects
// Returns Promise<A>
Effect.runPromise(effect)
// Returns A (throws on async or error)
Effect.runSync(effect)
// Returns Either<A, E>
Effect.runSyncEither(effect)
Next Steps
More Effect Resources