Basics

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)