Generators and Yield

Effect uses JavaScript generator functions to give you imperative-style code with full type safety. yield* extracts the success value of an effect, similar to how await unwraps a Promise.

Effect.gen

import { Effect } from "effect"

const program = Effect.gen(function*() {
  yield* Effect.log("Starting...")
  const a = yield* Effect.succeed(10)
  const b = yield* Effect.succeed(32)
  return a + b
})

Effect.runSync(program) // 42

yield* works with any Yieldable type: Effect, Option, Result, Config, and Context.Service. This gives you a uniform syntax for composing different Effect types.

The return yield* Pattern

When an effect terminates the workflow (errors, interrupts), you must use return yield* so TypeScript knows the code after it is unreachable:

import { Effect, Schema } from "effect"

class ValidationError extends Schema.TaggedError<ValidationError>()("ValidationError", {
  message: Schema.String
}) {}

const program = Effect.gen(function*() {
  const input = yield* Effect.succeed("")

  if (input.length === 0) {
    return yield* Effect.fail(new ValidationError({ message: "empty input" }))
  }

  // TypeScript knows this is reachable only if validation passed
  return input.toUpperCase()
})

Gotcha: Writing yield* Effect.fail(...) without return is a common bug. TypeScript won’t narrow the control flow, so it thinks the code after the failure is reachable. Always use return yield* for terminal effects.

// Wrong - TypeScript doesn't know this terminates
Effect.gen(function*() {
  if (bad) {
    yield* Effect.fail("error") // missing return!
    // TypeScript thinks this runs
  }
  return "ok"
})

// Correct - termination is explicit
Effect.gen(function*() {
  if (bad) {
    return yield* Effect.fail("error")
  }
  return "ok"
})

Effect.fn

Effect.fn creates named effect functions. The name improves stack traces and auto-attaches a tracing span. Pass combinators as additional arguments instead of using .pipe:

import { Effect, Schema } from "effect"

class FetchError extends Schema.TaggedError<FetchError>()("FetchError", {
  message: Schema.String
}) {}

export const fetchUser = Effect.fn("fetchUser")(
  function*(id: number): Effect.fn.Return<{ name: string }, FetchError> {
    yield* Effect.logInfo("Fetching user", id)
    return yield* Effect.tryPromise({
      try: () => fetch(`/api/users/${id}`).then(r => r.json()),
      catch: () => new FetchError({ message: "request failed" })
    })
  },
  // Combinators go here, NOT .pipe
  Effect.catchTag("FetchError", () => Effect.succeed({ name: "unknown" }))
)

// Usage
const result = yield* fetchUser(42)

Gotcha: Do not use .pipe with Effect.fn. Pass combinators as additional arguments to the Effect.fn call itself. Using .pipe breaks the tracing span and naming.

Effect.fnUntraced

Effect.fnUntraced is the same as Effect.fn but without tracing overhead. Use it for hot paths and library implementations where the tracing cost matters:

const parseLine = Effect.fnUntraced(function*(line: string) {
  const [key, value] = line.split(":")
  return { key: key.trim(), value: value.trim() }
})

When to Use What

ToolUse when
Effect.genInline composition, one-off operations, inside already-traced functions
Effect.fnNamed functions that return effects, when you want tracing and good stack traces
Effect.fnUntracedLibrary internals, hot paths, functions called many times per operation

Tip: Avoid writing functions that just return Effect.gen(...). Use Effect.fn or Effect.fnUntraced instead - you get better stack traces and tracing for free.

No try-catch in Generators

Effect handles errors through the type system, not JavaScript exceptions. Using try-catch inside Effect.gen breaks Effect’s error semantics:

// Wrong - try-catch doesn't work properly in Effect generators
Effect.gen(function*() {
  try {
    const result = yield* someEffect
    return result
  } catch (error) {
    // This may not be reached and breaks Effect semantics
    return null
  }
})

// Correct - use Effect.catch or Effect.result
Effect.gen(function*() {
  const result = yield* Effect.result(someEffect)
  if (result._tag === "Failure") {
    return null
  }
  return result.value
})

See The Effect Type for what yield* unwraps, and Typed Errors for how to handle failures.