Using Generators
Effect.gen lets you write effectful code in a familiar, imperative style -
the same shape as async/await, but for effects. You pass it a generator
function, and inside it you use yield* to run an effect and bind its success
value to a variable. Errors and service requirements are threaded through the
types for you, so the result is a single Effect describing the whole sequence.
import { Effect, Schema } from "effect"
// Use Schema.TaggedErrorClass to define a typed, tagged error.class FileProcessingError extends Schema.TaggedErrorClass<FileProcessingError>()( "FileProcessingError", { message: Schema.String }) {}
const program = Effect.gen(function*() { yield* Effect.log("Starting the file processing...") yield* Effect.log("Reading file...")
// Raising an error short-circuits the generator. Always `return` the // yielded error so TypeScript knows execution stops here and narrows the // types of anything after it. return yield* new FileProcessingError({ message: "Failed to read the file" })}).pipe( // Attach additional behaviour with `.pipe` after the generator. Effect.catch((error) => Effect.logError(`An error occurred: ${error}`)), Effect.withSpan("fileProcessing"))
Effect.runFork(program)How yield* works
Section titled “How yield* works”yield* is the heart of the syntax. Yielding an Effect<A, E, R>:
- runs that effect at this point in the sequence,
- binds its success value
Ato the left-hand side, - and accumulates its error type
Eand requirementsRinto the overall effect thatEffect.genreturns.
import { Effect } from "effect"
const divide = (a: number, b: number) => b === 0 ? Effect.fail("Cannot divide by zero" as const) : Effect.succeed(a / b)
// ┌─── Effect<number, "Cannot divide by zero">// ▼const program = Effect.gen(function*() { // Each yield* gives back the success value of the effect. const a = yield* Effect.succeed(10) const b = yield* Effect.succeed(2)
// If `divide` fails, the generator stops and the error becomes the // failure of the whole program - the next line never runs. const result = yield* divide(a, b)
yield* Effect.log(`10 / 2 = ${result}`) return result})Because yield* returns the plain success value, you can use ordinary
JavaScript in between: const/let, arithmetic, if, early return. You do
not need any special operators for non-effectful code.
Short-circuiting on failure
Section titled “Short-circuiting on failure”A generator runs top to bottom until something fails. The first failure stops
execution and becomes the failure of the entire effect - just like throw
unwinds an async function, but tracked in the type system.
import { Effect } from "effect"
const program = Effect.gen(function*() { yield* Effect.log("step 1") yield* Effect.fail("boom" as const) // execution stops here yield* Effect.log("step 2") // never runs})Always return yield* when yielding an effect that you intend as the failure
path (like a constructed error). The return tells TypeScript control will not
continue, which keeps later code correctly typed.
Yieldable values beyond Effect
Section titled “Yieldable values beyond Effect”Some data types are also yieldable inside a generator. For example, an
Option yields its inner value when it is Some, and
short-circuits with a NoSuchElementError when it is None:
import { Effect, Option } from "effect"
const lookup = (key: string): Option.Option<number> => key === "answer" ? Option.some(42) : Option.none()
const program = Effect.gen(function*() { // Yielding a `Some` produces its value... const x = yield* lookup("answer") // ...while a `None` fails the effect with NoSuchElementError. const y = yield* lookup("missing") return x + y})Using this inside a generator
Section titled “Using this inside a generator”If you need access to the surrounding object (for example inside a class
method), pass { self: this } as the first argument and the generator receives
it as this:
import { Effect } from "effect"
class Calculator { base = 100
total = Effect.gen({ self: this }, function*() { // `this` refers to the Calculator instance. const extra = yield* Effect.succeed(23) return this.base + extra })}When to reach for gen
Section titled “When to reach for gen”Use Effect.gen whenever a computation has multiple sequential steps, local
variables, or branching - it is the most readable way to express that. For a
function that returns an effect, prefer
Effect.fn over a plain function that returns
Effect.gen(...). For short transformations and attaching cross-cutting
behaviour, pipelines are often cleaner.