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.

bash
npm install effect

Output: added effect to dependencies

bash
pnpm add effect

Output: added 1 package, linked from store

bash
yarn add effect

Output: added effect

bash
bun add effect

Output: installed effect

Optional companions live under @effect/*:

bash
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/io and @effect/data packages)
  • 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

PackageTrade-off
Plain Promise + try/catchNative, zero overhead, no structured concurrency or DI.
neverthrowResult-typed errors only; smaller surface, no fibers or DI.
fp-tsEffect's spiritual predecessor — purer FP, less ergonomic. Largely superseded by Effect for new projects.
rxjsObservable 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.

typescript
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.

typescript
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

typescript
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

typescript
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.

typescript
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.

typescript
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

typescript
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 a Layer per environment (dev / staging / prod) and provide at the entry point.
  • Effect.runFork for long-running fibers; Effect.runPromise for one-shot.
  • Observability. @effect/platform integrates OpenTelemetry-compatible tracing; spans are automatic across Effect.gen boundaries.
  • Graceful shutdown. Effect.interrupt-aware fibers stop cleanly on SIGTERM. Use Runtime.runMain for 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.gen over deeply chained Effect.flatMap. The generator form is faster (single function dispatch) and easier to read.
  • Avoid Effect.runSync in hot paths inside async contexts. Mixing sync and async runners adds overhead.
  • Set concurrency on Effect.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.cachedWithTTL for memoising expensive computations.
  • Stream for backpressured pipelines. Effect's Stream module is fiber-aware and handles backpressure automatically.

Version migration guide

The biggest move was effect@2.xeffect@3.x.

FromToKey changes
@effect/io, @effect/data, etc (pre-2.0)effect@2.xConsolidated into a single effect package. Massive import path changes.
effect@2.xeffect@3.xImproved tree-shaking, refined APIs (some Effect.* helpers renamed), stricter generic inference.

Before (older @effect/io):

typescript
import * as Effect from "@effect/io/Effect";
import * as Layer from "@effect/io/Layer";

After (current effect):

typescript
import { Effect, Layer } from "effect";

Output: the unified effect package replaces the split @effect/io + @effect/data packages.

Migration checklist for 2 → 3:

  1. Bump effect and all @effect/* packages together — they version in lockstep within a major.
  2. Run TypeScript — inference tightened. Most fixes are renaming Effect.unsafeRunSyncEffect.runSync and similar.
  3. Layer construction signatures may have changed; check Layer.make / Layer.succeed call sites.
  4. Replace deprecated combinator names (consult the changelog).
  5. Bench any hot paths — 3.x is generally faster but profile to be sure.

Security considerations

  • Treat all Effect.tryPromise callbacks as untrusted code paths when they wrap user-controlled input. The error channel surfaces them as UnknownException — narrow with Effect.mapError before 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.gen makes it easy to compose; the order matters — validate, then authorise, then act.
  • Long-running fibers can leak. Always Fiber.interrupt or scope fibers under a parent so they're cleaned up on shutdown.
  • Effect.runSync and Effect.runFork swallow errors unless explicitly handled. Pipe through Effect.catchAll / Effect.tapError.

Testing & CI integration

Unit test with Vitest

typescript
// 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

typescript
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

PackageRole
@effect/schemaSchema validation, analog to zod
@effect/platform + adaptersHTTP, FS, process abstractions
@effect/sqlTyped SQL clients (PG, MySQL, SQLite)
@effect/rpcTyped RPC
@effect/clusterClustered actors / RPC
@effect/cliCLI framework
effect-http, effect-vitestCommunity 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 effectrunSync 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 importsimport { 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