Effect-TS turns TypeScript into a more functional language with typed errors, dependency injection, structured concurrency, and observable runtime. By 2026 it has a real production cohort. This post is the working knowledge and an honest take on when it pays off.
The pitch
Plain TypeScript:
async function getUser(id: number): Promise<User> {
const user = await db.users.findFirst({ where: eq(users.id, id) });
if (!user) throw new NotFoundError();
return user;
}
Errors are untyped. Dependencies are global. Retries / cancellation are manual.
Effect:
import { Effect, Schema, Cause } from "effect";
class NotFoundError { readonly _tag = "NotFoundError"; }
class DBError { readonly _tag = "DBError"; }
const getUser = (id: number) => Effect.gen(function* (_) {
const db = yield* _(DB); // dependency
const user = yield* _(db.findUser(id)); // typed error
return user;
});
// Type: Effect<User, NotFoundError | DBError, DB>
The type carries the success type, the failure types, and the required dependencies. The compiler enforces handling each one.
When it pays off
- Complex business logic with many error paths — typed errors prevent missed cases.
- Dependency injection at scale — the
DBtype above flows through; testing with a mock DB is a one-line swap. - Retries / concurrency — Effect’s runtime handles them as primitives, not callback hell.
- Observability — every Effect can be traced with structured spans.
When to skip
- Small CRUD app. Plain TypeScript + zod + Hono is faster to ship.
- Team unfamiliar with FP. The learning curve is real.
- Quick scripts.
A real example
import { Effect, Schedule } from "effect";
const fetchWithRetry = (url: string) =>
fetch(url).then(r => r.json()).pipe(
Effect.timeout("5 seconds"),
Effect.retry(Schedule.exponential("100 millis").pipe(Schedule.upTo("30 seconds"))),
Effect.tap(r => Effect.logInfo("fetched", { url }))
);
Timeout, retry with exponential backoff up to 30s, structured logging. Each combinator composable.
Schemas
Effect’s Schema competes with zod for validation:
import { Schema as S } from "effect";
const User = S.struct({
id: S.number,
email: S.string.pipe(S.pattern(/^[^@]+@[^@]+$/)),
name: S.string,
});
type UserT = S.Schema.Type<typeof User>;
const parsed = S.decodeSync(User)(unknownData);
Equivalent to zod for validation. Wins when you want Schema to integrate with Effects’ error types.
Concurrency
const fetchAll = Effect.all([
fetchUser(1),
fetchUser(2),
fetchUser(3),
], { concurrency: 2 }); // bounded concurrency
Structured concurrency is built in: cancel one, all are cancelled cleanly. Closer to Tokio than to bare Promise.all.
Where I’d reach for it
For 2026 backends:
- High-stakes business logic (financial, regulated). Typed errors save you.
- Complex agent / workflow code where retries, timeouts, dependencies multiply.
- Teams comfortable with FP who want one toolset for the whole stack.
For most CRUD: skip. Hono + Drizzle + zod (Modern TypeScript Backend with Hono on Bun ) covers it.
Read this next
- Modern TypeScript Backend with Hono on Bun
- Drizzle ORM Deep Dive
- Idempotency, Retries, and Exactly-Once Illusions
- Bun vs Node.js in 2026
If you want a Hono + Effect template, it’s at rajpoot.dev .
Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .