Effect-TS vs fp-ts (2026)
The author of fp-ts (Giulio Canti) joined the Effect organization — and Effect is now officially what fp-ts v3 would have been. That's not a casual endorsement. It's an acknowledgment that fp-ts solved the "what to use" question for typed functional programming in TypeScript, and Effect solved the "how to build real systems" question that fp-ts left partially answered.
TL;DR
fp-ts is the established foundation: excellent for typed functional composition, Option, Either, Task, and type-safe data transformations. Effect is the complete system: everything fp-ts does plus fiber-based concurrency, dependency injection, structured error handling, observability, and a runtime designed for production applications. fp-ts is being merged into the Effect ecosystem — Effect is the future.
Key Takeaways
- fp-ts: 3.1M weekly npm downloads, 11K GitHub stars — established, widely used
- Effect (
effectpackage): Growing rapidly, fp-ts creator joined the team - Giulio Canti (fp-ts creator) officially joined Effect team — Effect is the successor
- Effect has fiber-based concurrency; fp-ts does not
- Effect includes: error handling, dependency injection, streams, testing utilities, tracing
- fp-ts is more minimal and composable; Effect is more opinionated and batteries-included
- Learning curve: fp-ts is steep; Effect is very steep initially, then productive
Why Functional Programming in TypeScript?
Functional programming in TypeScript solves real production problems:
- Explicit error handling: Return
Either<Error, Value>instead of throwing - Composability: Chain operations without mutation side effects
- Testability: Pure functions are trivially testable
- Predictability: No hidden state, no surprise mutations
The question is which abstraction layer to use.
fp-ts: The Foundation
Package: fp-ts
Weekly downloads: 3.1M
GitHub stars: 11K
Creator: Giulio Canti
fp-ts brings Haskell's category theory-inspired abstractions to TypeScript: Option, Either, Task, Reader, State, IO, and monadic composition.
Core Data Types
import { Option, some, none, map, getOrElse } from 'fp-ts/Option';
import { Either, right, left, chain, mapLeft } from 'fp-ts/Either';
import { pipe } from 'fp-ts/function';
// Option: represents a value that may or may not exist
const findUser = (id: string): Option<User> => {
const user = db.get(id);
return user ? some(user) : none;
};
const userName = pipe(
findUser('123'),
map(user => user.name),
getOrElse(() => 'Unknown User')
);
// Either: represents success OR failure
const parseAge = (input: string): Either<string, number> => {
const age = parseInt(input);
if (isNaN(age)) return left(`"${input}" is not a valid age`);
if (age < 0) return left('Age cannot be negative');
return right(age);
};
const validateUser = (data: unknown): Either<string, User> =>
pipe(
parseAge(String(data.age)),
map(age => ({ ...data, age }))
);
Task: Async Operations
import { Task } from 'fp-ts/Task';
import { TaskEither } from 'fp-ts/TaskEither';
import * as TE from 'fp-ts/TaskEither';
// TaskEither: async operation that may fail
const fetchUser = (id: string): TaskEither<Error, User> =>
TE.tryCatch(
() => fetch(`/api/users/${id}`).then(r => r.json()),
(error) => new Error(String(error))
);
// Chain operations
const getUserName = (id: string): TaskEither<Error, string> =>
pipe(
fetchUser(id),
TE.map(user => user.name),
TE.mapLeft(error => new Error(`Failed to get user: ${error.message}`))
);
// Execute the effect
const main = async () => {
const result = await getUserName('123')();
// result is Either<Error, string>
if (result._tag === 'Right') {
console.log(result.right); // the name
} else {
console.error(result.left); // the error
}
};
fp-ts Limitations
fp-ts has real production limitations:
- No built-in concurrency model (parallel async tasks require manual orchestration)
- No dependency injection system (Reader monad works but is verbose)
- No streaming primitives
- No built-in retry, timeout, or circuit breaker patterns
- Steep learning curve + verbose pipe chains
Effect: The Production System
Package: effect
GitHub stars: 9K (growing fast)
Creator: Effect team (including Giulio Canti, formerly fp-ts)
Effect is a complete functional programming platform for TypeScript. It includes everything fp-ts has plus a production-grade runtime with fiber-based concurrency, structured error handling, dependency injection, and observability.
The Effect Type
The core type is Effect<A, E, R>:
A: The success value typeE: The expected error typeR: The required services/dependencies
import { Effect, pipe } from 'effect';
// Effect that returns string, may fail with Error, requires DatabaseService
type GetUserName = Effect.Effect<string, Error, DatabaseService>;
Basic Usage
import { Effect, Console } from 'effect';
// Create effects
const hello = Effect.succeed('Hello, World!');
const fail = Effect.fail(new Error('Something went wrong'));
// Transform effects
const greeting = pipe(
Effect.succeed({ name: 'Alice', age: 30 }),
Effect.map(user => `Hello, ${user.name}!`),
Effect.flatMap(greeting => Console.log(greeting))
);
// Run effects
import { runPromise } from 'effect/Effect';
await runPromise(greeting); // Logs "Hello, Alice!"
Structured Error Handling
Effect's error handling is more expressive than fp-ts:
import { Effect, Data } from 'effect';
// Tagged error classes (discriminated unions)
class UserNotFoundError extends Data.TaggedError('UserNotFoundError')<{ userId: string }> {}
class DatabaseError extends Data.TaggedError('DatabaseError')<{ cause: unknown }> {}
const findUser = (userId: string): Effect.Effect<User, UserNotFoundError | DatabaseError> =>
Effect.tryPromise({
try: () => db.findUser(userId),
catch: (error) => new DatabaseError({ cause: error }),
}).pipe(
Effect.flatMap(user =>
user ? Effect.succeed(user) : Effect.fail(new UserNotFoundError({ userId }))
)
);
// Handle errors by type
const result = findUser('123').pipe(
Effect.catchTag('UserNotFoundError', (err) =>
Effect.succeed({ name: 'Anonymous', id: err.userId })
),
Effect.catchTag('DatabaseError', (err) =>
Effect.fail(new Error(`DB error: ${err.cause}`))
)
);
Dependency Injection
Effect's most powerful feature for production apps:
import { Effect, Context, Layer } from 'effect';
// Define services
class DatabaseService extends Context.Tag('DatabaseService')<
DatabaseService,
{ findUser: (id: string) => Promise<User | null> }
>() {}
class EmailService extends Context.Tag('EmailService')<
EmailService,
{ send: (to: string, subject: string, body: string) => Promise<void> }
>() {}
// Use services in effects (R type captures dependencies)
const sendWelcomeEmail = (userId: string): Effect.Effect<
void,
Error,
DatabaseService | EmailService
> =>
Effect.gen(function* () {
const db = yield* DatabaseService;
const email = yield* EmailService;
const user = yield* Effect.promise(() => db.findUser(userId));
if (!user) yield* Effect.fail(new Error(`User ${userId} not found`));
yield* Effect.promise(() =>
email.send(user!.email, 'Welcome!', `Hello ${user!.name}!`)
);
});
// Provide implementations (like a DI container)
const DatabaseServiceLive = Layer.succeed(DatabaseService, {
findUser: (id) => db.users.findById(id),
});
const EmailServiceLive = Layer.succeed(EmailService, {
send: (to, subject, body) => sendgrid.send({ to, subject, text: body }),
});
// Run with dependencies
const program = sendWelcomeEmail('123').pipe(
Effect.provide(Layer.merge(DatabaseServiceLive, EmailServiceLive))
);
await Effect.runPromise(program);
Fiber-Based Concurrency
Effect's killer feature for production systems:
import { Effect, Fiber } from 'effect';
// Parallel execution
const parallel = Effect.all([
fetchUserProfile(userId),
fetchUserOrders(userId),
fetchUserPreferences(userId),
], { concurrency: 'unbounded' }); // All three run in parallel
// Race (first to complete wins)
const raceResult = Effect.race(
fetchFromPrimaryDB(id),
fetchFromReplicaDB(id)
);
// Timeout
const withTimeout = fetchData().pipe(
Effect.timeout('5 seconds')
);
// Retry with backoff
const withRetry = fetchData().pipe(
Effect.retry(Schedule.exponential(1000).pipe(Schedule.take(3)))
);
// Interrupt on cancellation (fibers automatically clean up)
const fiber = yield* Effect.fork(longRunningProcess());
// ... later
yield* Fiber.interrupt(fiber); // Clean shutdown
Effect.gen: Readable Async Code
Effect.gen uses generators for readable, synchronous-looking async code:
const processOrder = (orderId: string) =>
Effect.gen(function* () {
// Looks like synchronous code, runs as async Effect
const order = yield* getOrder(orderId);
const user = yield* getUser(order.userId);
const inventory = yield* checkInventory(order.items);
if (!inventory.available) {
yield* Effect.fail(new InsufficientInventoryError({ orderId }));
}
yield* chargePayment(user.paymentMethod, order.total);
yield* updateOrderStatus(orderId, 'confirmed');
yield* sendConfirmationEmail(user.email, order);
return { success: true, orderId };
});
The fp-ts to Effect Migration Path
Since fp-ts is merging with Effect, there's a clear migration guide:
// fp-ts: Option
import { Option, some, none, map } from 'fp-ts/Option';
const opt: Option<number> = some(42);
const doubled = map((n: number) => n * 2)(opt);
// Effect: Option
import { Option } from 'effect';
const opt = Option.some(42);
const doubled = Option.map(opt, n => n * 2);
// fp-ts: Either
import { Either, right, left, chain } from 'fp-ts/Either';
const result: Either<Error, User> = right(user);
// Effect: Either
import { Either } from 'effect';
const result = Either.right(user);
When to Use Each
Use fp-ts if:
- Your codebase already has fp-ts and migration isn't feasible
- You want the minimal, composable building blocks
- Your team is comfortable with the Haskell-inspired API
- You need only data transformation (Option, Either, Task) without a runtime
Use Effect if:
- Starting a new TypeScript project with functional patterns
- Production concerns: retries, timeouts, circuit breakers are needed
- Dependency injection without a separate DI framework
- Concurrency: parallel, racing, fork/join patterns
- You want built-in telemetry and tracing
- Testing with dependency injection (easy to swap implementations in tests)
Learning Curve and Team Adoption
The learning curve for both libraries is genuinely steep, and honestly assessing this before adoption saves significant team friction. fp-ts requires comfort with category theory concepts — functors, monads, applicatives — even if you never need to know those terms formally. The pipe function and the point-free style take 2–4 weeks for most TypeScript developers to read fluently. The payoff is that once internalized, the patterns are consistent and composable: Option, Either, TaskEither, and Reader all follow the same functor/monad laws, so learning one teaches the pattern for all of them.
Effect's learning curve is steeper initially but has a different shape. The Effect<A, E, R> type signature is more complex than fp-ts's simpler types, and the generator-based Effect.gen syntax requires understanding how yield* interacts with the Effect runtime. However, Effect's comprehensive documentation (the official Effect documentation is significantly better than fp-ts's) and the large number of beginner-friendly examples in the community mean that developers who commit to learning Effect typically become productive within 3–6 weeks. Effect's Slack community and the Effect Days conference have created a support ecosystem that fp-ts never had at comparable scale.
For team adoption, the fp-ts-to-Effect transition is smoother than adopting either from scratch because Effect's core types (Option, Either) are deliberately compatible with fp-ts conventions. Teams can migrate incrementally: keep fp-ts for existing data transformation code, adopt Effect for new service-level code that needs concurrency and dependency injection, and gradually migrate fp-ts code to Effect as it needs modification. The fp-ts interop module in Effect provides conversion utilities between fp-ts types and Effect types, enabling the two approaches to coexist in a single codebase during the transition period.
Production Patterns: Error Modeling
How errors are modeled is one of the most consequential design decisions in a functional TypeScript codebase, and Effect's approach is substantially more expressive than fp-ts's Either<E, A> type. In fp-ts, the E type parameter in Either<E, A> is typically Error or a union of error message strings — broad types that lose specificity and require instanceof checks or message-string matching to handle different error cases. Effect's tagged error pattern using Data.TaggedError creates discriminated union error types where each error case has a unique string tag and typed fields, enabling exhaustive pattern matching with the TypeScript compiler verifying that all error cases are handled.
In production systems with complex business logic — order processing, payment workflows, multi-step data transformation — the difference between TaskEither<Error, Order> and Effect<Order, OutOfStockError | PaymentDeclinedError | InsufficientInventoryError> is the difference between "something went wrong" and "exactly this type of business failure occurred, and here are the specific details." Effect's Effect.catchTag('OutOfStockError', handler) handles only that specific error case, leaving others to propagate — a structural approach that prevents the common fp-ts pattern of catching all Left values and then manually inspecting the error to determine the appropriate response. Teams building systems where different business errors require fundamentally different recovery strategies (retry, notify user, rollback transaction) benefit most from Effect's more expressive error type system.
Ecosystem and Community Momentum
The community momentum around these two libraries has shifted decisively in Effect's favor since Giulio Canti joined the Effect team. The fp-ts repository has slowed in terms of new features — the library is considered feature-complete by its maintainers, with future development focused on compatibility and bug fixes rather than new capabilities. Effect, by contrast, has been releasing major improvements — the effect package now includes Schema (a Zod alternative), Stream (a reactive streams library), and Platform (OS-level abstractions for file system, HTTP client/server, and terminal) as integrated components. This expanding scope means teams adopting Effect get a comprehensive functional programming platform rather than a single utility library.
The fp-ts ecosystem of companion libraries — io-ts for runtime validation, monocle-ts for optics, fp-ts-contrib for utilities — is being superseded by Effect's built-in equivalents. io-ts users in particular are encouraged to migrate to @effect/schema, which provides the same codec-based runtime validation with better error messages and Effect integration. Teams evaluating whether to invest in fp-ts or Effect for a new codebase in 2026 should factor in this trajectory: fp-ts is mature and stable but has a narrowing future as its creator's attention moves to Effect, while Effect has active development, growing documentation, and a clear roadmap toward covering all the functional programming needs that previously required assembling multiple fp-ts companion packages.
The Bottom Line
fp-ts was the right answer for functional TypeScript for years. Effect is the right answer for 2026 and beyond — especially now that the fp-ts creator is building it. If you're starting a new project that benefits from functional programming patterns (explicit errors, immutability, composition), start with Effect.
Compare these packages on PkgPulse.
Compare Effect and fp-ts package health on PkgPulse.
See also: Effect-TS vs fp-ts 2026 and Effect-TS vs fp-ts vs Neverthrow: TS Errors 2026, AI SDK vs LangChain: Which to Use in 2026.
See the live comparison
View effect ts vs. fp ts on PkgPulse →