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(...)withoutreturnis a common bug. TypeScript won’t narrow the control flow, so it thinks the code after the failure is reachable. Always usereturn 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
.pipewithEffect.fn. Pass combinators as additional arguments to theEffect.fncall itself. Using.pipebreaks 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
| Tool | Use when |
|---|---|
Effect.gen | Inline composition, one-off operations, inside already-traced functions |
Effect.fn | Named functions that return effects, when you want tracing and good stack traces |
Effect.fnUntraced | Library internals, hot paths, functions called many times per operation |
Tip: Avoid writing functions that just return
Effect.gen(...). UseEffect.fnorEffect.fnUntracedinstead - 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.