Effect-TS: Handle Errors and Dependencies With Full Type Safety in TypeScript

Effect-TS tracks errors and dependencies in the type system - no more unknown catch blocks, no more implicit dependencies - here is how to adopt it in a real TypeScript project.

Mahmudul Haque Qudrati — CEO & ML Engineer at Pristren

Mahmudul Haque Qudrati

CEO & ML Engineer

May 10, 2026
7 min read
Effect-TS: Handle Errors and Dependencies With Full Type Safety in TypeScript

The Problem Effect Solves

One AI engineering post, weekly

LLM benchmarks, prompt techniques, and token-cost breakdowns — not another AI news roundup.

TypeScript has excellent type checking for the happy path. But try/catch gives you an unknown error type - the failure path is completely untyped. And dependencies (database clients, HTTP clients, config) are typically passed through function arguments or grabbed from globals - invisible to the type system.

Effect-TS solves both. Every Effect has three type parameters: Effect<Success, Error, Requirements>. The compiler tracks what can go wrong and what is needed to run.

The Effect Type

typescript
import { Effect } from "effect";

// Effect<User, UserNotFound | DatabaseError, DatabaseClient>
// Success: User
// Error: UserNotFound | DatabaseError
// Requirements: DatabaseClient
function getUser(id: string): Effect.Effect<User, UserNotFound | DatabaseError, DatabaseClient> {
  return Effect.gen(function* () {
    const db = yield* DatabaseClient;
    const user = yield* db.findUser(id);
    if (!user) yield* Effect.fail(new UserNotFound({ id }));
    return user;
  });
}

Team workspace

Ship faster with chat, meetings, and projects in one place — Zlyqor.

Start free

Effect.gen() - Async/Await Syntax

Effect.gen() provides generator-based syntax that reads like async/await but with typed errors:

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

const program = Effect.gen(function* () {
  const user = yield* getUser("user-123");    // typed: User | fails with UserNotFound
  const posts = yield* getUserPosts(user.id); // typed: Post[] | fails with NotFound
  yield* Console.log(`${user.name} has ${posts.length} posts`);
  return { user, posts };
});

// Run it  -  provide requirements, handle errors
Effect.runPromise(
  program.pipe(
    Effect.provide(DatabaseLive),     // satisfy DatabaseClient requirement
    Effect.catchTag("UserNotFound", (e) =>
      Effect.succeed({ user: null, posts: [] })
    ),
  )
);

Typed Error Channel

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

// Define typed errors
class UserNotFound extends Data.TaggedError("UserNotFound")<{ id: string }> {}
class DatabaseError extends Data.TaggedError("DatabaseError")<{ cause: unknown }> {}

const fetchUser = (id: string) =>
  Effect.tryPromise({
    try: () => db.users.findOne(id),
    catch: (e) => new DatabaseError({ cause: e }),
  }).pipe(
    Effect.flatMap((user) =>
      user ? Effect.succeed(user) : Effect.fail(new UserNotFound({ id }))
    )
  );

// Handle specific error types
fetchUser("123").pipe(
  Effect.catchTag("UserNotFound", () => Effect.succeed(defaultUser)),
  Effect.catchTag("DatabaseError", (e) => {
    console.error(e.cause);
    return Effect.fail(new ServiceUnavailable());
  })
);

Dependency Injection via Layer

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

// Define a service interface
class EmailService extends Context.Tag("EmailService")<
  EmailService,
  { sendWelcome: (email: string) => Effect.Effect<void> }
>() {}

// Live implementation
const EmailServiceLive = Layer.succeed(
  EmailService,
  {
    sendWelcome: (email) =>
      Effect.tryPromise({
        try: () => nodemailer.sendMail({ to: email, subject: "Welcome!" }),
        catch: (e) => new EmailError({ cause: e }),
      }),
  }
);

// Test implementation
const EmailServiceTest = Layer.succeed(
  EmailService,
  { sendWelcome: (_email) => Effect.void }
);

// Use in program  -  compiler requires EmailService to be provided
const registerUser = (email: string) =>
  Effect.gen(function* () {
    const mailer = yield* EmailService;
    yield* mailer.sendWelcome(email);
  });

Retry, Timeout, and Schedule

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

const resilientFetch = fetchUser("123").pipe(
  Effect.timeout("5 seconds"),
  Effect.retry(
    Schedule.exponential("100 millis").pipe(
      Schedule.intersect(Schedule.recurs(3))  // max 3 retries with exponential backoff
    )
  ),
);

When to Adopt Effect-TS

Good fit:

  • New TypeScript projects where you control the whole stack
  • Server-side Node.js code with complex error scenarios
  • Replacing fp-ts (Effect is the spiritual successor)
  • Teams that want to eliminate runtime surprises

Not the right fit:

  • Browser bundles (Effect adds ~50KB gzipped)
  • Projects mixing plain JS and TypeScript
  • When you just need simple error handling - use neverthrow instead

References: Effect · GitHub · docs

#effect-ts#typed-errors#dependency-injection#typescript#functional

// discussion

Comments

0/4000
Mahmudul Haque Qudrati — CEO & ML Engineer at Pristren

Mahmudul Haque Qudrati

CEO & ML Engineer

Visionary technologist, software engineer, and machine learning specialist. Founder and CEO of Pristren, directing engineering teams that ship production-grade AI/ML pipelines, mission-critical full-stack applications, and developer tooling. Creator of Zlyqor, the unified team workspace platform. Author of 540+ technical guides and benchmark research reports on large language models, agentic workflows, Model Context Protocol (MCP), and modern web stacks.

PristrenZlyqor