Skip to content

What it is

Effect is a library for building typed, composable programs where errors, dependencies and asynchrony all appear in the type signature.

An Effect<Success, Error, Requirements> is a description of a computation, not a running one. Nothing executes until you run it, which makes retries, timeouts and cancellation composable.

Installation

npm install effect

Getting started

The smallest useful thing you can do with it, and what each part means.

Errors and dependencies in the type
import { Effect, Data } from 'effect';

class NotFound extends Data.TaggedError('NotFound')<{ id: string }> {}
class NetworkError extends Data.TaggedError('NetworkError')<{ cause: unknown }> {}

const getBook = (id: string): Effect.Effect<Book, NotFound | NetworkError> =>
  Effect.tryPromise({
    try: () => fetch(`/api/books/${id}`).then((r) => r.json()),
    catch: (cause) => new NetworkError({ cause }),
  }).pipe(
    Effect.flatMap((book) =>
      book ? Effect.succeed(book) : Effect.fail(new NotFound({ id })),
    ),
  );
The signature lists every way this can fail. A plain Promise<Book> tells you nothing about failure, which is the gap Effect closes.
Composing and running
const program = getBook('42').pipe(
  Effect.retry({ times: 3 }),
  Effect.timeout('5 seconds'),
  Effect.catchTag('NotFound', () => Effect.succeed(defaultBook)),
  Effect.tap((book) => Effect.log(`loaded ${book.title}`)),
);

// Nothing has run yet — this is a description.
const book = await Effect.runPromise(program);
catchTag handles one specific error and removes it from the type. Retry and timeout are ordinary combinators rather than bespoke code at each call site.

Advanced usage

Where the library earns its place over a simpler alternative.

Services and structured concurrency
class Database extends Effect.Service<Database>()('Database', {
  effect: Effect.gen(function* () {
    const pool = yield* acquirePool;
    return { query: (sql: string) => Effect.promise(() => pool.query(sql)) };
  }),
}) {}

const load = Effect.gen(function* () {
  const db = yield* Database;             // dependency appears in the type
  const [books, authors] = yield* Effect.all(
    [db.query('SELECT * FROM books'), db.query('SELECT * FROM authors')],
    { concurrency: 'unbounded' },          // run together
  );
  return { books, authors };
});

await Effect.runPromise(load.pipe(Effect.provide(Database.Default)));
Effect.gen reads like async/await but tracks errors and requirements. If one branch of Effect.all fails, the others are interrupted automatically — structured concurrency by default.

Errors and fixes

The failures you are most likely to hit, and what actually resolves them.

Nothing happens when the effect is created
Effects are descriptions. They only execute under runPromise, runSync or runFork.
Type error about a missing requirement R
A service the effect needs was not provided. Add Effect.provide with the corresponding layer.

Best practices

  • Adopt it at a module boundary, not incrementally everywhere — half-Effect code is confusing.
  • Use Data.TaggedError so catchTag can discriminate failures.
  • Prefer Effect.gen over long pipe chains once there is branching.
  • Be honest about the learning curve; it is the steepest of any library here.

Background

Why it exists, and what it was reacting to.

Inspired by Scala's ZIO, Effect gives TypeScript a single abstraction covering async, error handling, dependency injection, retries, concurrency and resource safety — at the cost of a substantial new vocabulary.