Effect.ts for TypeScript services
Effect.ts for TypeScript services
Section titled “Effect.ts for TypeScript services”Related: Cloud agent orchestrator · Effect skill · 2026-09-28 Pi SDK v2 abilities boundary and data model · Bend skill
Pin checked 2026-09-28: effect@3.22.2 (latest); v4 under rc / beta (4.0.0-rc.*) — docs site has a version selector. Default this stack to 3.x stable unless a deliberate v4 spike.
Primary docs: effect.website · docs hub · GitHub Effect-TS/effect
Why it fits coordy’s orchestrator
Section titled “Why it fits coordy’s orchestrator”| Concern | Effect role | Bend / other |
|---|---|---|
| Legal run transitions | — | Bend SM (LAWS/PROOF) |
| Ask legality → append ledger → I/O → effect receipt | Executor as Effect pipeline | SQLite/Drizzle TX still host |
Retries on git / gh / flaky settle | Effect.retry + Schedule | — |
| Worktree / Pi session / DB handle cleanup | acquireRelease + Scope | — |
| Hono / MCP / CLI entry | ManagedRuntime.runPromise | Framework stays Promise-shaped |
| SSE / event fan-out | Stream | — |
| Wire Config / Db / Git / BendBridge | Context.Tag / Effect.Service + Layer | Test layers swap fakes |
| OpenAPI / event payloads | effect/Schema | — |
Effect does not replace Bend proofs or Drizzle SQL. It structures how TS side effects compose, fail, retry, and clean up.
Core concepts (Effect 3)
Section titled “Core concepts (Effect 3)”Effect<A, E, R>
Section titled “Effect<A, E, R>”Lazy description of a workflow: succeeds with A, fails with typed E, needs services R. Creating an effect does nothing until a Runtime runs it (Effect.runPromise, runFork, runSync, or ManagedRuntime).
Compose with Effect.gen(function* () { … }) (generators) or pipe operators. Prefer Data.TaggedError("…") for domain errors.
Docs: The Effect Type
Errors — two channels
Section titled “Errors — two channels”| Kind | In E? | Typical use |
|---|---|---|
| Expected (failures) | Yes | Domain: TransitionIllegal, PrOpenFailed, DbConflict |
| Unexpected (defects) | No | Bugs, thrown exceptions → Cause; recover with catchAllCause / exit |
Handle expected errors with catchTag / catchTags / either / match. Docs: Two Types of Errors
Layer + services
Section titled “Layer + services”Layer<Out, E, In> constructs services without leaking construction deps into service method signatures (keep ops Effect<…, …, never>). Compose with Layer.provide / merge / provideMerge. App entry: build MainLive, then Effect.provide(program, MainLive) or wrap in ManagedRuntime.
Shorthand: Effect.Service generates tag + .Default layer. Docs: Managing Layers
Scope + resources
Section titled “Scope + resources”Effect.acquireRelease(acquire, release) ties cleanup to a Scope; Effect.scoped opens/closes the scope (finalizers run reverse order, including on fail/interrupt). Use for worktrees, Pi dispose, DB clients, temp dirs. Docs: Scope
Schedule
Section titled “Schedule”Immutable recurrence policy for Effect.retry / Effect.repeat (backoff, spaced, forever ∩ capped, etc.). Compose with union/intersect. Docs: Scheduling
Stream<A, E, R>
Section titled “Stream<A, E, R>”Like Effect but 0..n values — backpressured pipelines; stand-in for AsyncIterable / Observable-style flows (SSE chunks, log tails). Docs: Streams
Schema (effect/Schema)
Section titled “Schema (effect/Schema)”Schema<Type, Encoded, Requirements> for decode/encode/assert (HTTP bodies, event payloads, config). Prefer over ad-hoc Zod-at-boundary-only if the rest of the app is Effect. Docs: Schema introduction
Runtime / ManagedRuntime
Section titled “Runtime / ManagedRuntime”Runtime interprets effects (fibers, interruption, finalizers). Default Effect.run* uses empty context. For servers: ManagedRuntime.make(appLayer) once, then runPromise / runFork per request; dispose on shutdown. Docs: Runtime
When to use vs plain async / Promise
Section titled “When to use vs plain async / Promise”Reach for Effect when
- Failures must appear in types and branch in one place (illegal transition vs I/O fail vs bug).
- You need retries / timeouts / interruption with shared policy (
Schedule). - Resources must release on every exit path (worktree, session, TX helpers).
- Services should swap Live/Test via Layers without rewriting call sites.
- Concurrency is structured (bounded
Effect.forEach, race, fibers) not ad-hocPromise.all+ forgotten abort.
Stay on Promise / sync when
- Trivial glue at a framework edge already typed by Hono/Drizzle.
- One-shot scripts with no retries/DI (or call
runPromiseonce at the end). - A library API is Promise-only and wrapping every call adds noise without gain — wrap at the service boundary, not every line.
Anti-pattern: “Effect everywhere” including pure helpers, or await Effect.runPromise inside every nested function (destroys composition and scopes).
Testing
Section titled “Testing”@effect/vitest(peer: effect ^3.22, vitest ^3) — helpers to run Effects in Vitest (npm).- Provide Test layers (
Layer.succeed/ fakes) for Git, BendBridge, clock; assert onExit/Cause/ tagged errors. - Prefer testing the program with injected layers over mocking internals of Live layers.
Ecosystem (relevant packages)
Section titled “Ecosystem (relevant packages)”| Package | Role |
|---|---|
effect | Core (Effect, Layer, Schema, Stream, Schedule, Scope, ManagedRuntime, …) |
@effect/platform + @effect/platform-node | FS, HTTP client, Command, Path — Node impls |
@effect/sql (+ sqlite drivers) | Optional SQL Effect integration (orchestrator may keep Drizzle; optional later) |
@effect/vitest | Test helpers |
@effect/opentelemetry | Tracing/metrics if/when panels need it |
Install note (upstream): TypeScript strict; Node 18+ (some SQL packages want newer Node — check package readme; stack already targets Node 24).
Sketch — executor step (illustrative)
Section titled “Sketch — executor step (illustrative)”// Pseudocode — not a locked APIconst applyTransition = (cmd: TransitionCmd) => Effect.gen(function* () { const bend = yield* BendBridge const db = yield* Ledger const legal = yield* bend.check(cmd) // Effect; Bend = policy if (!legal) return yield* Effect.fail(new TransitionIllegal({ cmd })) yield* db.transact(cmd) // append transition + projection same TX const receipt = yield* Effect.acquireRelease( runSideEffect(cmd), // worktree / Pi / gh (handle, exit) => cleanup(handle, exit), ).pipe(Effect.scoped) yield* db.appendEffect(receipt) }).pipe( Effect.retry(Schedule.intersect(Schedule.exponential("100 millis"), Schedule.recurs(3))), )
// Hono edgeconst runtime = ManagedRuntime.make(MainLive)app.post("/runs", async (c) => { const body = Schema.decodeUnknownSync(CreateRun)(await c.req.json()) const result = await runtime.runPromise(applyTransition(body)) return c.json(result)})Bend still decides legality; Effect owns retry, cleanup, and service wiring around that call.
Pitfalls
Section titled “Pitfalls”- Running effects inside effects via nested
runPromise— break scopes and fibers. - Putting construction requirements on service methods (Layer leak) — keep method
R = never. - Treating defects as domain errors (or swallowing
Cause). - Using Effect retries to paper over illegal Bend transitions — those should fail typed, not retry.
- Adopting Effect v4 RC for production without a spike — pin 3.22.x until v4 is
latest. - Expecting Effect to sandbox processes — still OS/worktree/
spawnHookPATH policy.