Guidelines for agents and developers new to Effect in this repository. Follow these rules to write readable, testable, and type-safe code that fits the Effect ecosystem.
- Effect-first: Model all async/IO with
Effect. Avoidasync/awaitin library code; useEffect.genor pipeable combinators. - Pipe-first: Favor the pipeable API (
pipe(…)) for pure transforms and small chains; useEffect.genwhen sequencing many steps or mixing awaits and service access. - Typed errors: Model domain errors with
Schema.TaggedError. Never throw untyped errors from Effect-based APIs; fail with typed errors instead. - Dependency injection: Inject dependencies via
Effect.Service(preferred) orContext.Tag, and provide them withLayer. - Resource safety: Manage lifecycles with
Layer.scoped,Effect.acquireRelease, andScope. Don’t leak resources. - Edges only run: Only call
Effect.run*at program edges (CLIs, servers, scripts, tests). Library code should returnEffect.
- When to use: Prefer
Effect.genfor readable, sequential workflows and service access.- Example:
Effect.gen(function* () { const db = yield* Db; const rows = yield* db.query(sql); return rows })
- Example:
- Sequential semantics: Each
yield*is sequential. For parallelism, combine effects withEffect.all,Effect.tuple, or fork fibers.- Example:
const [a, b] = yield* Effect.all([fa, fb]) - Example:
const fiber = yield* Effect.fork(task); const res = yield* Fiber.join(fiber)
- Example:
- Service access:
yield* ServiceTagoryield* MyServiceto access a service from context (see Services below). - Error handling:
- Convert exceptions:
yield* Effect.try({ try: f, catch: (cause) => new MyError({ cause }) }) - Map error types:
program.pipe(Effect.mapError((e) => e instanceof Error ? new MyError({ cause: e }) : e)) - Capture result:
const exit = yield* Effect.exit(program); assert or branch onExitin tests.
- Convert exceptions:
- Resource safety in gen: Use
Effect.acquireReleaseinsideEffect.gen, or preferLayer.scopedfor service setup. - Anti-patterns: Don’t mix
awaitwithEffect.gen. Don’tyield*non-Effect values. Don’t run effects inside constructors; build effects that construct values instead.
Prefer Effect.Service to declare services with their default Layer and (optional) accessors.
import { Effect, Layer } from "effect"
// 1) Define a service with accessors and dependencies
class Logger extends Effect.Service<Logger>()("Logger", {
accessors: true,
effect: Effect.gen(function* () {
return {
info: (msg: string) => Effect.sync(() => console.log(msg))
}
})
}) {}
// 2) Provide the default layer where you run
const program = Logger.info("Hello").pipe(Effect.provide(Logger.Default))- Accessors: With
accessors: true, methods on the service become functions returningEffect, e.g.Logger.info("msg"). - Dependencies: Add
dependencies: [OtherService.Default, …]. UseDefaultto include deps; useDefaultWithoutDependenciesto get the bare layer. - Scoped services: Use
scopedwhen the service acquires resources that must be released (files, sockets, pools). Provide withLayer.launchin runtime orit.scopedin tests. - Use as Tag: You can
yield* LoggerinsideEffect.gento get the concrete implementation if you prefer OO-style usage. - When to use Context.Tag: Use
Context.Tagfor simple DI tokens or when you don’t need a prebuilt default layer/accessors. Otherwise preferEffect.Servicefor ergonomics. - Testing: Provide stubs via
Layer.succeed(Service, Service.make(stub))or define aTestlayer on your service class.
Example (stubbing):
class Clock extends Effect.Service<Clock>()("Clock", {
accessors: true,
sync: () => ({ now: () => Date.now() })
}) {
static Test = Layer.succeed(this, new Clock({ now: () => 123 }))
}
// In test: Effect.provide(Clock.Test)Use Schema.TaggedError to define typed, serializable error classes with consistent _tag and schema.
import { Schema, Effect } from "effect"
class UserNotFound extends Schema.TaggedError<UserNotFound>()(
"UserNotFound",
{ id: Schema.String }
) {}
class Unauthorized extends Schema.TaggedError<Unauthorized>()(
"Unauthorized",
{}
) {}
// Failing with a tagged error
const findUser = (id: string) => Effect.fail(new UserNotFound({ id }))- Message: You may define a
get message()in the class for custom rendering, or include amessage: Schema.Stringfield. - Cause: Use
cause: Schema.Defectfield if you want to carry an underlying throwable (Error, unknown failure) for diagnostics. - Composable unions: Use union types of TaggedErrors for function error channels, e.g.
Effect<A, UserNotFound | Unauthorized, R>. - At boundaries: Map third-party errors into your domain errors at module edges. Inside your domain, raise your own tagged errors only.
- HTTP APIs: When using
@effect/platformHttp APIs, add error schemas via.addError(Unauthorized, { status: 401 }), etc.
Use @effect-native/bun-test or @effect/vitest to write tests as Effects without runPromise.
- Red: Start with a failing
it.effecttest that returns anEffect.- Example:
import { it, expect } from "@effect-native/bun-test" import { Effect } from "effect" it.effect("divides numbers", () => Effect.gen(function* () { const result = yield* divide(4, 2) expect(result).toBe(2) }) )
- Example:
- Green: Implement with
Effect.gen, inject dependencies viaLayer, and fail withSchema.TaggedErrorwhere needed. - Refactor: Extract dependencies into services, enable
accessors, compose layers, keep tests fast/deterministic with stubs andTestClock.
Key testing practices:
- it.effect: Default mode; injects
TestContext(e.g.TestClock). Place assertions inside the returnedEffect. - it.scoped: Use when the program needs a
Scope(e.g.Effect.acquireRelease,Layer.scoped). - it.live: Run with live environment for integration-style checks (e.g. real time, console). Use sparingly.
- Failures: Capture exits with
Effect.exitor useit.effect.failsfor temporarily expected failures. - Time control: Use
TestClock.adjust("1 second")to advance simulated time deterministically. - Stubs: Provide test layers:
Effect.provide([Service1.Test, Service2.Default])orLayer.succeed(Service, Service.make(stub)).
Example (service + test):
// service
class UserRepo extends Effect.Service<UserRepo>()("UserRepo", {
accessors: true,
effect: Effect.succeed({
findById: (id: string) => Effect.fail(new UserNotFound({ id }))
})
}) {}
// test
import { it, expect } from "@effect-native/bun-test"
import { Effect, Exit } from "effect"
it.effect("findById returns not found", () =>
Effect.gen(function* () {
const exit = yield* Effect.exit(UserRepo.findById("1").pipe(Effect.provide(UserRepo.Default)))
expect(exit).toStrictEqual(Exit.fail(new UserNotFound({ id: "1" })))
})
)-
Do:
- Use
Effect.genfor readable sequencing and service access. - Use
Effect.Servicewithaccessors: trueand provide viaService.Defaultin callers/tests. - Model all domain errors with
Schema.TaggedErrorand document them. - Compose
Layers in app wiring; keep library code context-polymorphic. - Keep tests hermetic with stubs,
TestClock, andit.effect.
- Use
-
Don’t:
- Don’t call
Effect.run*in libraries; only at app edges and tests. - Don’t throw untyped errors; map them into tagged errors.
- Don’t use global singletons; use services + layers.
- Don’t mix
async/awaitwith Effect. Use Effect operators throughout.
- Don’t call
- Access a service:
const svc = yield* MyService - Use accessor:
yield* MyService.doThing(arg)(withaccessors: true) - Provide deps:
Effect.provide([A.Default, B.Default]) - Parallel:
yield* Effect.all([fa, fb, fc]) - Exit:
const exit = yield* Effect.exit(eff) - Tagged error:
class E extends Schema.TaggedError<E>()("E", { msg: Schema.String }) {} yield* Effect.fail(new E({ msg: "bad" }))
- Test:
it.effect("name", () => Effect.gen(function* () { /* assertions */ }))