Guidelines
Most Effect code follows a handful of conventions that keep it readable, traceable,
and testable. This page walks through the ones that matter most day to day: how
to write generators, when to wrap them in Effect.fn, why async/await and
try/catch have no place inside an effect, why behavior belongs in services
rather than module-level globals, and why you read the time from Clock instead
of Date.
import { Clock, Context, Effect, Layer, Schema } from "effect"
// Errors are schema-tagged classes. `Schema.Defect` captures an unknown cause.class ChargeError extends Schema.TaggedErrorClass<ChargeError>()("ChargeError", { cause: Schema.Defect}) {}
// Behavior lives in a service, not in free functions reaching for globals.class Payments extends Context.Service<Payments, { readonly charge: (cents: number) => Effect.Effect<string, ChargeError>}>()("app/Payments") { static readonly layer = Layer.effect( Payments, Effect.gen(function*() { // A function that returns an Effect is written with Effect.fn, not a // plain arrow that returns Effect.gen. The name feeds stack traces and // opens a tracing span automatically. const charge = Effect.fn("Payments.charge")(function*(cents: number) { // Read the time from Clock — never `Date.now()` — so tests can control it. const now = yield* Clock.currentTimeMillis yield* Effect.log(`Charging ${cents} cents at ${now}`) // `return yield*` makes the failure terminal and keeps the types honest. if (cents <= 0) { return yield* new ChargeError({ cause: "amount must be positive" }) } return `txn_${now}` })
return Payments.of({ charge }) }) )}The rest of this page explains each of these choices.
Effect.gen for imperative composition
Section titled “Effect.gen for imperative composition”Effect.gen lets you write effectful code in a familiar, imperative style. Each
yield* runs an effect and unwraps its success value, much like await unwraps a
promise — but with full error and requirement tracking in the type.
import { Effect } from "effect"
const program = Effect.gen(function*() { yield* Effect.log("Starting...") const a = yield* Effect.succeed(1) const b = yield* Effect.succeed(2) return a + b})Use Effect.gen for inline composition — one-off pipelines, the body of a layer,
or steps that won’t be reused on their own.
Effect.fn for functions that return an effect
Section titled “Effect.fn for functions that return an effect”When you write a named function whose job is to produce an effect, wrap the
generator with Effect.fn(name) instead of returning a bare Effect.gen.
import { Effect } from "effect"
// The name improves stack traces and attaches a span (via Effect.withSpan).const greet = Effect.fn("greet")(function*(name: string) { yield* Effect.log(`Hello, ${name}`) return name.length})import { Effect } from "effect"
// No name, no span, worse stack traces. Avoid this shape.const greet = (name: string) => Effect.gen(function*() { yield* Effect.log(`Hello, ${name}`) return name.length })You add extra behavior by passing further arguments to Effect.fn — do not
use .pipe on the result:
import { Effect } from "effect"
const greet = Effect.fn("greet")( function*(name: string) { return name.length }, // Each additional argument is applied like a pipe step. Effect.annotateLogs({ method: "greet" }))When you write inline Effect.gen (not wrapped in a function), add spans and
adjustments with .pipe as usual:
import { Effect } from "effect"
Effect.gen(function*() { yield* Effect.log("working")}).pipe(Effect.withSpan("work"))No async/await or try/catch inside effects
Section titled “No async/await or try/catch inside effects”Effect models concurrency and failure in the type system. Mixing in JavaScript’s own mechanisms breaks that model.
try/catch does not work inside Effect.gen: a failed effect short-circuits the
generator rather than throwing a catchable exception, so the catch block never
runs. Use Effect’s error handling instead.
import { Effect } from "effect"
declare const risky: Effect.Effect<number, Error>
// ❌ The catch never fires — Effect failures aren't thrown exceptions.// Effect.gen(function*() {// try {// return yield* risky// } catch (e) {// return 0// }// })
// ✅ Inspect the outcome as a value with Effect.result...const handled = Effect.gen(function*() { const result = yield* Effect.result(risky) return result._tag === "Success" ? result.success : 0})
// ✅ ...or recover with a combinator. `Effect.catch` is the catch-all.const recovered = risky.pipe(Effect.catch(() => Effect.succeed(0)))Likewise, never reach for async/await. To lift a promise into Effect, use
Effect.promise (for promises that cannot reject meaningfully) or
Effect.tryPromise (to capture a rejection as a typed error):
import { Effect } from "effect"
declare function loadUser(id: string): Promise<{ name: string }>
const user = Effect.tryPromise({ try: () => loadUser("u_1"), catch: (cause) => new Error(`failed to load user: ${cause}`)})Services over module-level globals
Section titled “Services over module-level globals”Anything your code depends on — a database handle, an HTTP client, configuration, even the current time — should be reached through a service rather than a global singleton imported at the top of a module. Globals are invisible to the type system, impossible to swap in tests, and tie initialization order to import order.
import { Context, Effect, Layer } from "effect"
class Config extends Context.Service<Config, { readonly apiUrl: string}>()("app/Config") { static readonly layer = Layer.succeed(Config, Config.of({ apiUrl: "https://api.example.com" }))}
// The dependency shows up in the Effect's requirements (`R`) channel, so the// compiler forces you to provide it — and lets a test provide a fake.const fetchUrl = Effect.fn("fetchUrl")(function*(path: string) { const config = yield* Config return `${config.apiUrl}${path}`})For configuration values, prefer Effect’s Configuration layer;
for singletons with a sensible default that callers may override, use
Context.Reference. See Services & Layers for the
complete pattern.
Clock over Date
Section titled “Clock over Date”Reading the wall clock directly (Date.now(), new Date()) makes code
non-deterministic and untestable: every run produces different timestamps and you
cannot fast-forward time. Effect exposes the clock as a service so tests can
substitute a TestClock.
import { Clock, Effect } from "effect"
const stampedEvent = Effect.fn("stampedEvent")(function*(message: string) { // Pulls from the ambient Clock service, controllable in tests. const millis = yield* Clock.currentTimeMillis return { message, at: millis }})In tests, TestClock lets you advance time explicitly so time-dependent logic —
timeouts, retries, schedules — runs instantly and deterministically. See
Testing and Scheduling for how this plays out in
practice.
Avoid tacit (point-free) style
Section titled “Avoid tacit (point-free) style”Write the function explicitly rather than passing it bare to a combinator:
import { Effect } from "effect"
declare const parse: (s: string) => numberdeclare const effect: Effect.Effect<string>
// ✅ Explicit — robust to overloads and clearer in stack traceseffect.pipe(Effect.map((s) => parse(s)))
// ⚠️ Tacit — can erase generics on overloaded functions and worsen inferenceeffect.pipe(Effect.map(parse))Tacit usage looks tidy, but when the passed function has overloads or optional parameters it can silently drop generics and produce surprising types. Spelling out the argument is a cheap habit that prevents a class of subtle bugs.