Skip to content

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.

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)

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.

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.