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
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
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.
Effect.gen() - Async/Await Syntax
Effect.gen() provides generator-based syntax that reads like async/await but with typed errors:
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
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
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
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

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.
More from Mahmudul
Related Articles
How to Build an MCP Server in TypeScript (Step-by-Step)
Build your first MCP server in TypeScript with the official SDK — tools, stdio transport, and a Claude Code test loop in under an hour.
Next.js App Router Patterns in 2026: What to Use and What to Avoid
App Router is the default in Next.js. Here are the patterns that work well in production and the ones that create more problems than they solve.
React Server Components: What They Are and When to Use Them
React Server Components run only on the server and never ship to the browser. Here is what that means in practice and when it actually helps.
// discussion
Comments