Effect-TS vs fp-ts: Functional Programming in TypeScript 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.
See the live comparison
View effect ts vs. fp ts on PkgPulse →