Here are some guidelines for how to structure basic Effect code. How to express sequencing with Effect.gen, and when to name effectful functions with Effect.fn.
Effect.gen
Just as async/await provides a sequential, readable way to work with Promise values (avoiding nested .then() chains), Effect.gen and yield* provide the same ergonomic benefits for Effect values.
import { Effect } from "effect"
const program = Effect.gen(function* () {
const data = yield* fetchData
yield* Effect.logInfo(`Processing data: ${data}`)
return yield* processData(data)
})Effect.fn
Use Effect.fn with generator functions for traced, named effects. Effect.fn traces where the function is called from, not just where it's defined:
import { Effect } from "effect"
const processUser = Effect.fn("processUser")(function* (userId: string) {
yield* Effect.logInfo(`Processing user ${userId}`)
const user = yield* getUser(userId)
return yield* processData(user)
})Effect.fn also accepts a second argument: a function that transforms the entire effect. This is useful for adding cross-cutting concerns like timeouts without wrapping the body:
import { Effect, flow, Schedule } from "effect"
const fetchWithTimeout = Effect.fn("fetchWithTimeout")(
function* (url: string) {
const data = yield* fetchData(url)
return yield* processData(data)
},
flow(
Effect.retry(Schedule.recurs(3)),
Effect.timeout("5 seconds")
)
)Benefits:
- Call-site tracing for each invocation
- Stack traces with location details
- Clean signatures
Note: Effect.fn automatically creates spans that integrate with telemetry systems.
Pipe for Instrumentation
Use .pipe() to add cross-cutting concerns to Effect values. Common uses: timeouts, retries, logging, and annotations.
import { Effect, Schedule } from "effect"
const program = fetchData.pipe(
Effect.timeout("5 seconds"),
Effect.retry(Schedule.exponential("100 millis").pipe(Schedule.compose(Schedule.recurs(3)))),
Effect.tap((data) => Effect.logInfo(`Fetched: ${data}`)),
Effect.withSpan("fetchData")
)Common instrumentation:
- Effect.timeout - fail if effect takes too long
- Effect.retry - retry on failure with a schedule
- Effect.tap - run side effect without changing the value
- Effect.withSpan - add tracing span
Retry and Timeout
For production code, combine retry and timeout to handle transient failures:
import { Effect, Schedule } from "effect"
// Retry with exponential backoff, max 3 attempts
const retryPolicy = Schedule.exponential("100 millis").pipe(
Schedule.compose(Schedule.recurs(3))
)
const resilientCall = callExternalApi.pipe(
// Timeout each individual attempt
Effect.timeout("2 seconds"),
// Retry failed attempts
Effect.retry(retryPolicy),
// Overall timeout for all attempts
Effect.timeout("10 seconds")
)Schedule combinators:
- Schedule.exponential - exponential backoff
- Schedule.recurs - limit number of retries
- Schedule.spaced - fixed delay between retries
- Schedule.compose - combine schedules (both must continue)