Effect.fn
When you write a function that returns an Effect, use Effect.fn("name").
It lets you use the generator syntax for the body, and in exchange for the name
you give it, it attaches a tracing span and improves stack traces - both
invaluable when debugging a real application. Avoid the common anti-pattern of
writing a plain function that returns Effect.gen(...); Effect.fn is the
idiomatic replacement.
import { Effect, Schema } from "effect"
class SomeError extends Schema.TaggedErrorClass<SomeError>()("SomeError", { message: Schema.String}) {}
// The string should match the function name. It becomes the span name and// shows up in stack traces.export const effectFunction = Effect.fn("effectFunction")( // `Effect.fn.Return<A, E>` documents the success and error types. It takes // the same type parameters as `Effect.Effect`. function*(n: number): Effect.fn.Return<string, SomeError> { yield* Effect.logInfo("Received number:", n)
// Always `return` the yielded error so TypeScript knows execution stops. return yield* new SomeError({ message: "Failed to read the file" }) }, // Additional combinators are passed as extra arguments to Effect.fn - // do NOT use `.pipe` on an Effect.fn definition. Effect.catch((error) => Effect.logError(`An error occurred: ${error}`)), Effect.annotateLogs({ method: "effectFunction" }))Why not a plain function returning Effect.gen?
Section titled “Why not a plain function returning Effect.gen?”These two look almost identical:
import { Effect } from "effect"
// Anti-pattern: a plain function returning Effect.gen.const badGreet = (name: string) => Effect.gen(function*() { yield* Effect.log(`Hello, ${name}`) })
// Idiomatic: Effect.fn with a name.const greet = Effect.fn("greet")(function*(name: string) { yield* Effect.log(`Hello, ${name}`)})greet automatically wraps its body in a span called "greet" (as if you had
written Effect.withSpan("greet")) and records the call site for richer stack
traces. badGreet gets none of that. Since named functions are exactly the
boundaries you want to see in a trace, Effect.fn is the right default. See
Observability for what those spans give you.
Arguments and the generator body
Section titled “Arguments and the generator body”The arguments you call the function with become the arguments of the generator.
Inside the body you write the same yield*-based code as in
Effect.gen:
import { Effect } from "effect"
const divide = Effect.fn("divide")(function*(a: number, b: number) { if (b === 0) { return yield* Effect.fail("Cannot divide by zero" as const) } return a / b})
// ┌─── Effect<number, "Cannot divide by zero">// ▼const program = divide(10, 2)Adding behaviour with extra arguments
Section titled “Adding behaviour with extra arguments”Anything you would normally .pipe onto an effect, you instead pass as extra
arguments after the generator. They are applied in order, wrapping the body:
import { Effect } from "effect"
const fetchPage = Effect.fn("fetchPage")( function*(url: string) { yield* Effect.log(`fetching ${url}`) return `<html>${url}</html>` }, // Retry the whole body on failure, then add log annotations. Effect.retry({ times: 3 }), Effect.annotateLogs({ component: "fetchPage" }))This keeps the span, retry policy, and annotations together with the function definition, which is exactly where you want them.
Effect.fnUntraced
Section titled “Effect.fnUntraced”If you specifically do not want a span - for a hot, frequently-called
helper where the tracing overhead is unwelcome - use Effect.fnUntraced. It has
the same generator-based shape but skips the span and naming. Reach for it only
when you have a reason to; Effect.fn("name") is the better default.