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)
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 the live comparison
View effect ts vs. fp ts on PkgPulse →