cheat sheet
effect
Package-level reference for the Effect library — Effect values, generators, fibers, error channels, dependency injection, and the 2.x → 3.x migration.
effect
What it is
effect is a TypeScript library for writing structured, type-safe asynchronous and concurrent programs. An Effect<A, E, R> is a description of a program that may produce a value of type A, may fail with an error of type E, and may require an environment / dependency context R. The library provides fibers (lightweight processes), structured concurrency, dependency injection, retry / scheduling primitives, and a generator-based syntax that feels like async/await but composes far better.
Reach for effect when error handling, retries, concurrency control, and resource lifetimes are first-class concerns — long-running services, agent orchestration, retry-heavy pipelines. Reach for plain async/await + try/catch if the program is small; the conceptual overhead of effect pays for itself only on larger systems.
Install
effect ships as a single monorepo package containing the core and many sub-modules.
npm install effect
Output: added effect to dependencies
pnpm add effect
Output: added 1 package, linked from store
yarn add effect
Output: added effect
bun add effect
Output: installed effect
Optional companions live under @effect/*:
npm install @effect/platform @effect/schema
Output: adds platform abstractions (HTTP, FS) and schema validation
Versioning & Node support
Current line is effect@3.x (released 2024 after the 2.x stabilisation work). The 3.x line is the stable target — 2.x was the first npm-published rebrand of the older @effect/io packages.
- Node 18+ recommended; works in modern browsers, Deno, Bun, and edge runtimes (when the modules you use don't reach for Node-only APIs).
- ESM + CJS dual published; ESM is the modern path.
- TypeScript types bundled.
- Strict semver in
3.x; sub-packages under@effect/*may version slightly out of step with the core.
Package metadata
- Maintainer: Effect-TS team (Michael Arnaldi and contributors)
- Project home: github.com/Effect-TS/effect
- Docs: effect.website
- npm: npmjs.com/package/effect
- License: MIT
- First released: 2023 (consolidating earlier
@effect/ioand@effect/datapackages) - Downloads: hundreds of thousands weekly — rapidly growing in production TypeScript codebases.
Peer dependencies & extras
effect has no peer dependencies. Common sub-packages:
@effect/schema— schema validation and parser (analog to zod)@effect/platform— runtime-agnostic HTTP / FS / Process / Path abstractions@effect/platform-node/@effect/platform-browser/@effect/platform-bun— runtime adapters@effect/sql,@effect/sql-pg,@effect/sql-sqlite-node— typed SQL clients@effect/cluster— clustered actor / RPC support@effect/rpc— typed RPC@effect/cli— CLI framework with Effect at the core
Alternatives
| Package | Trade-off |
|---|---|
Plain Promise + try/catch | Native, zero overhead, no structured concurrency or DI. |
neverthrow | Result-typed errors only; smaller surface, no fibers or DI. |
fp-ts | Effect's spiritual predecessor — purer FP, less ergonomic. Largely superseded by Effect for new projects. |
rxjs | Observable streams — overlapping concepts but stream-first. |
zio (Scala) | Same model in Scala. Different language. |
Real-world recipes
Simple Effect.gen flow
Effect.gen is the generator-based syntax — analogous to async/await but for effects.
import { Effect, Console } from "effect";
const program = Effect.gen(function* () {
yield* Console.log("starting");
const value = yield* Effect.succeed(42);
yield* Console.log(`got ${value}`);
return value;
});
const result = await Effect.runPromise(program);
console.log(result);
Output: prints starting, then got 42, then 42 from the outer log.
Error channel with Effect.fail
Errors are values in the error channel, not thrown.
import { Effect } from "effect";
class NotFound { readonly _tag = "NotFound" }
function findUser(id: string): Effect.Effect<string, NotFound> {
return id === "1" ? Effect.succeed("Alice") : Effect.fail(new NotFound());
}
const program = findUser("2").pipe(
Effect.catchTag("NotFound", () => Effect.succeed("guest")),
);
console.log(await Effect.runPromise(program));
Output: prints guest; the NotFound tag is caught and replaced with a default.
Concurrent runs with Effect.all
import { Effect } from "effect";
const fetchA = Effect.promise(() => fetch("https://api.example.com/a").then((r) => r.json()));
const fetchB = Effect.promise(() => fetch("https://api.example.com/b").then((r) => r.json()));
const program = Effect.all([fetchA, fetchB], { concurrency: "unbounded" });
const [a, b] = await Effect.runPromise(program);
Output: both requests run concurrently; on success, the tuple is returned; on failure, the first error short-circuits and any running fibers are interrupted.
Retry with exponential backoff
import { Effect, Schedule, Duration } from "effect";
const flaky = Effect.tryPromise(() => fetch("https://api.example.com/flaky"));
const policy = Schedule.exponential(Duration.millis(100)).pipe(Schedule.compose(Schedule.recurs(5)));
const program = flaky.pipe(Effect.retry(policy));
const res = await Effect.runPromise(program);
Output: retries up to 5 times with exponentially increasing delays starting from 100 ms; if all retries fail the error surfaces.
Resource management with Effect.acquireRelease
Resources opened in acquireRelease are guaranteed to be released even on failure / interrupt.
import { Effect } from "effect";
const useDb = Effect.acquireUseRelease(
Effect.sync(() => ({ id: 1, close: () => console.log("closed") })),
(db) => Effect.sync(() => `using db ${db.id}`),
(db) => Effect.sync(() => db.close()),
);
console.log(await Effect.runPromise(useDb));
Output: prints using db 1, then closed — release runs even if use throws or is interrupted.
Dependency injection with Context
A Context.Tag declares a service; Effect.provide supplies the implementation.
import { Effect, Context } from "effect";
class Logger extends Context.Tag("Logger")<Logger, { log: (m: string) => Effect.Effect<void> }>() {}
const program = Effect.gen(function* () {
const logger = yield* Logger;
yield* logger.log("hello");
});
const LoggerLive = Logger.of({ log: (m) => Effect.sync(() => console.log(`[live] ${m}`)) });
const main = program.pipe(Effect.provideService(Logger, LoggerLive));
await Effect.runPromise(main);
Output: prints [live] hello; swap the live implementation for a mock in tests.
Schema validation with @effect/schema
import { Schema } from "@effect/schema";
import { Effect } from "effect";
const User = Schema.Struct({
email: Schema.String.pipe(Schema.pattern(/@/)),
age: Schema.Number.pipe(Schema.greaterThan(17)),
});
const parse = Schema.decodeUnknown(User);
const result = await Effect.runPromise(parse({ email: "alice@example.com", age: 30 }));
console.log(result);
Output: prints the validated object; invalid input rejects with a structured ParseError.
Production deployment
Effect is a library — the deployment story is your application's. The Effect-specific concerns are runtime configuration, observability, and graceful shutdown.
Layers for environments. Compose service implementations into aLayerper environment (dev / staging / prod) andprovideat the entry point.Effect.runForkfor long-running fibers;Effect.runPromisefor one-shot.- Observability.
@effect/platformintegrates OpenTelemetry-compatible tracing; spans are automatic acrossEffect.genboundaries. - Graceful shutdown.
Effect.interrupt-aware fibers stop cleanly onSIGTERM. UseRuntime.runMainfor the right wiring. - Bundle size. Effect is large (~80 KB minified core). For browser code that uses only small parts, configure your bundler for aggressive tree-shaking and import from specific submodules.
Performance tuning
- Use
Effect.genover deeply chainedEffect.flatMap. The generator form is faster (single function dispatch) and easier to read. - Avoid
Effect.runSyncin hot paths inside async contexts. Mixing sync and async runners adds overhead. - Set
concurrencyonEffect.all/Effect.forEach. Default is sequential; specify{ concurrency: 8 }or"unbounded"based on the workload. - Cache services in Layers, not per-request. Layers are lazily initialised once per runtime.
Effect.cached/Effect.cachedWithTTLfor memoising expensive computations.Streamfor backpressured pipelines. Effect's Stream module is fiber-aware and handles backpressure automatically.
Version migration guide
The biggest move was effect@2.x → effect@3.x.
| From | To | Key changes |
|---|---|---|
@effect/io, @effect/data, etc (pre-2.0) | effect@2.x | Consolidated into a single effect package. Massive import path changes. |
effect@2.x | effect@3.x | Improved tree-shaking, refined APIs (some Effect.* helpers renamed), stricter generic inference. |
Before (older @effect/io):
import * as Effect from "@effect/io/Effect";
import * as Layer from "@effect/io/Layer";
After (current effect):
import { Effect, Layer } from "effect";
Output: the unified effect package replaces the split @effect/io + @effect/data packages.
Migration checklist for 2 → 3:
- Bump
effectand all@effect/*packages together — they version in lockstep within a major. - Run TypeScript — inference tightened. Most fixes are renaming
Effect.unsafeRunSync→Effect.runSyncand similar. - Layer construction signatures may have changed; check
Layer.make/Layer.succeedcall sites. - Replace deprecated combinator names (consult the changelog).
- Bench any hot paths — 3.x is generally faster but profile to be sure.
Security considerations
- Treat all
Effect.tryPromisecallbacks as untrusted code paths when they wrap user-controlled input. The error channel surfaces them asUnknownException— narrow withEffect.mapErrorbefore the value crosses a trust boundary. - Service tags can be spoofed in tests. Make sure production runtime
Layers aren't accidentally included in test bundles. - Schema validation should run before authorisation.
Effect.genmakes it easy to compose; the order matters — validate, then authorise, then act. - Long-running fibers can leak. Always
Fiber.interruptor scope fibers under a parent so they're cleaned up on shutdown. Effect.runSyncandEffect.runForkswallow errors unless explicitly handled. Pipe throughEffect.catchAll/Effect.tapError.
Testing & CI integration
Unit test with Vitest
// program.test.ts
import { describe, it, expect } from "vitest";
import { Effect, Layer } from "effect";
import { Logger } from "./Logger";
const TestLogger = Logger.of({ log: () => Effect.unit });
describe("program", () => {
it("runs", async () => {
const program = Effect.gen(function* () {
const logger = yield* Logger;
yield* logger.log("hello");
return 1;
});
const result = await Effect.runPromise(program.pipe(Effect.provideService(Logger, TestLogger)));
expect(result).toBe(1);
});
});
Output: test passes; the test logger swallows the message without side effects.
Test the error channel
import { Effect, Exit } from "effect";
it("fails on bad input", async () => {
const program = Effect.fail("nope" as const);
const exit = await Effect.runPromiseExit(program);
if (Exit.isFailure(exit)) {
expect(exit.cause).toBeDefined();
}
});
Output: runPromiseExit returns the full Exit data structure so you can introspect cause / failure / interrupt.
Ecosystem integrations
| Package | Role |
|---|---|
@effect/schema | Schema validation, analog to zod |
@effect/platform + adapters | HTTP, FS, process abstractions |
@effect/sql | Typed SQL clients (PG, MySQL, SQLite) |
@effect/rpc | Typed RPC |
@effect/cluster | Clustered actors / RPC |
@effect/cli | CLI framework |
effect-http, effect-vitest | Community integrations |
Troubleshooting common errors
Type instantiation is excessively deep — TypeScript hit its inference budget on a complex pipe chain. Break the pipe into intermediate const bindings.
No service for tag "X" found — forgot to provide the implementation via Effect.provideService / Layer. Trace through the call site.
Effect.runSync called with an async effect — runSync requires the entire Effect to be synchronous. Switch to runPromise or remove the async boundary.
Hang on shutdown — an unhandled fiber kept running. Use Effect.runFork and explicitly Fiber.interrupt on SIGTERM.
Cannot find module 'effect' under tsconfig bundler moduleResolution — install effect and confirm the package's exports map matches your moduleResolution; bundler is the recommended setting.
Slow imports — import { Effect } from "effect" pulls a fair amount via barrel exports. With aggressive tree-shaking enabled (esbuild / rollup / vite), unused modules drop. With Webpack 4, prefer specific imports like import * as Effect from "effect/Effect".
When NOT to use this
- Tiny scripts. A 30-line script doesn't benefit from the conceptual overhead. Use
async/await. - Teams unwilling to learn the model. Effect requires up-front investment; half-buy-in leads to mixed code that is worse than either pure approach.
- Bundle-size-critical browser code. Effect's core is ~80 KB minified. For widgets, neverthrow + plain Promises is leaner.
- Existing codebases without test coverage. Migrating to Effect mid-project is high-risk; introduce it at module boundaries first.
See also
- JavaScript: promises — what Effect builds on top of
- Concept: async — concurrency, structured concurrency, fibers