Last updated: June 01, 2026
At first glance Effect reads as “fp-ts v3” — the same typed-functional toolkit one version newer — yet the two libraries overlap only at the typed-values layer. That gap is the hidden cost: adopting Effect “because it’s the successor” quietly commits your whole codebase to a runtime mental model your team has to learn. The honest answer to effect vs fp-ts is that they are not two versions of one library.
More on Effect Fp Ts.
fp-ts is a functional algebra toolkit — typed Either, Option, Task, plus typeclasses like Monoid and Traversable — and it stays out of your architecture. Effect is an application runtime where the Effect<A, E, R> type becomes your whole program: fibers, interruption, Layer-based dependency injection, built-in observability.
Choose fp-ts when you only need composable typed values; choose Effect when you need the runtime.
- Different category, not different version. fp-ts = algebra (HKT, typeclasses); Effect = runtime (fibers, Layer, Scope). The shared surface is just the typed-values layer.
- “fp-ts v3” is the vendor’s framing, not the author’s. The merger is real, but fp-ts’s maintainer says migrate only when the benefits beat the cost.
- Two learning walls, not one. fp-ts’s wall is typeclass abstraction; Effect’s wall is the runtime mental model.
- Error tracking differs in handling, not in “having it.” Both type the error channel; only Effect gives you
catchTagto peel one error off a union. - Bundle cost is real but tunable. Effect’s
Micromodule is a lighter subset that omits the fiber runtime (per the Micro for Effect users guide); full Effect pulls the runtime back in.
Effect vs fp-ts in one decision: algebra toolkit or application runtime?
The single pivot that decides this: do you need the runtime, or just the algebra? Everything else is downstream of that question.
fp-ts gives you values. Either<E, A> is a value that is either an error or a result. TaskEither<E, A> is a lazy async version. You compose them with pipe and flow, and nothing about your program’s execution model changes — you’re still writing ordinary functions that happen to return well-typed containers. The library has no opinion about how your app starts, how concurrency works, or how dependencies get wired.
Effect gives you a program description. An Effect<A, E, R> is a blueprint that, when run by Effect’s runtime, produces an A, may fail with an E, and requires services R. That runtime brings fiber-based structured concurrency, interruption, retries, and observability hooks.
The official Effect vs fp-ts comparison lists numerous dimensions where Effect ticks the box and fp-ts ticks only a few — but read closely: rows like Metrics, Tracing, Caching, and Fiber Supervision are runtime features fp-ts never set out to provide. The grid measures scope, not quality.
Purpose-built diagram for this article — Effect vs fp-ts for typed functional TypeScript: error tracking and learning curve compared.
The diagram above maps the two libraries onto the same surface: fp-ts occupies the “values and combinators” region, while Effect spans that region and the runtime region above it. The overlap — typed errors, pipeable APIs, an option type — is the small shared zone. Most of Effect’s footprint is territory fp-ts deliberately leaves empty.
The “Effect is fp-ts v3” claim, adjudicated
The merger is genuine; the “v3” label is marketing shorthand that the author himself is more careful about. Both can be true, and reconciling them is the part most write-ups skip.
The facts: the fp-ts README states the project is “officially merging with the Effect-TS ecosystem” and that Effect “can be regarded as the successor to fp-ts v2.” Giulio Canti, fp-ts’s creator, joined the Effect organization — the “A bright future for Effect” announcement spells out that there will be no separate fp-ts v3; the communities consolidated on Effect.
Related: the satisfies operator.
The nuance: fp-ts is not abandoned, and migration is not mandatory. In the fp-ts and effect-ts discussion (#1852), the guidance is that moving to Effect is worth it only if the benefits outweigh the migration cost — and community members in that thread push back hard on treating Effect as a drop-in successor, pointing out how little API surface actually overlaps. The fp-ts v2 line kept shipping maintenance releases well after the merger; the fp-ts releases page shows v2.16.x tags landing into late 2024.
So “is Effect fp-ts v3?” — organizationally, yes, that’s where the energy went. Practically, no: it’s a re-architecture, not a version bump. Treat a move from fp-ts to Effect as adopting a new runtime, budgeted like one.
Error tracking, head to head
Both libraries type their error channel — that’s the shared part. The difference shows up when you want to recover from one error out of a union and let the rest keep propagating. In fp-ts you narrow by hand; in Effect, catchTag does it and rewrites the error type for you.
Here is the same two-error pipeline in fp-ts. A load can fail as NotFound or Forbidden, and recovery means switching on the _tag yourself:
If this matters to your setup, typing values in a catch block is worth a look.
import { pipe } from "fp-ts/function"
import * as E from "fp-ts/Either"
interface NotFound { readonly _tag: "NotFound"; readonly id: string }
interface Forbidden { readonly _tag: "Forbidden"; readonly role: string }
type LoadError = NotFound | Forbidden
const loadUser = (id: string): E.Either<LoadError, { id: string }> =>
id === "0" ? E.left({ _tag: "NotFound", id }) : E.right({ id })
const checkAccess = (user: { id: string }): E.Either<LoadError, { id: string }> =>
user.id === "1" ? E.left({ _tag: "Forbidden", role: "guest" }) : E.right(user)
const program = (id: string): E.Either<LoadError, { id: string }> =>
pipe(loadUser(id), E.chain(checkAccess))
// Recover from NotFound only — manual _tag narrowing, and the
// return type stays Either<LoadError, ...>: Forbidden is NOT removed.
const handled = pipe(
program("0"),
E.orElse((err) =>
err._tag === "NotFound"
? E.right({ id: "fallback" })
: E.left(err) // Forbidden re-thrown by hand
)
)
Notice the cost: you re-emit Forbidden manually, and the inferred error type still includes both variants. The compiler can’t tell that you’ve “dealt with” NotFound, because nothing structurally removed it.
Now Effect, using Data.TaggedError and catchTag as documented in the Effect expected-errors guide:
import { Effect, Data } from "effect"
class NotFound extends Data.TaggedError("NotFound")<{ readonly id: string }> {}
class Forbidden extends Data.TaggedError("Forbidden")<{ readonly role: string }> {}
const loadUser = (id: string) =>
id === "0" ? Effect.fail(new NotFound({ id })) : Effect.succeed({ id })
const checkAccess = (user: { id: string }) =>
user.id === "1" ? Effect.fail(new Forbidden({ role: "guest" })) : Effect.succeed(user)
const program = (id: string) =>
loadUser(id).pipe(Effect.flatMap(checkAccess))
// program: Effect<{ id: string }, NotFound | Forbidden, never>
const handled = program("0").pipe(
Effect.catchTag("NotFound", () => Effect.succeed({ id: "fallback" }))
)
// handled: Effect<{ id: string }, Forbidden, never> -- NotFound removed from E
Effect tracks the possible failures as a union automatically, and catchTag("NotFound", ...) subtracts that variant from the error channel — the inferred type becomes Effect<{ id: string }, Forbidden, never>. That subtraction is the practical edge: the type system keeps an accurate ledger of what’s still unhandled, which is exactly what “typed errors” should buy you and what the fp-ts version makes you maintain manually.

The benchmark panel above contrasts the two axes the title names — error-handling ergonomics and onboarding cost. fp-ts wins on smallness and conceptual minimalism for the error case; Effect wins on the mechanical correctness of union subtraction at the cost of more concepts to learn first.
Dependency injection: Effect accumulates and checks requirements before you run
Dependency injection is where the two diverge most sharply at the type level. In fp-ts, the environment parameter R (as in ReaderTaskEither<R, E, A>) is something you thread and combine yourself with fairly basic tooling. In Effect, every API that touches a service folds that requirement into the R channel for you, and the program will not type-check as runnable until each accumulated requirement has been provided by a Layer.
In practice, fp-ts makes you compose the environment by hand. If one function needs a Database and another needs a Logger, the combined function’s R must be declared as the intersection Database & Logger yourself. There is no built-in guarantee that two services demanding the same key with conflicting shapes get caught.
A related write-up: DI container resolution failures.
Effect derives the requirement set for you:
import { Effect, Context } from "effect"
class Database extends Context.Tag("Database")<
Database, { readonly query: (sql: string) => Effect.Effect<string> }
>() {}
class Logger extends Context.Tag("Logger")<
Logger, { readonly log: (msg: string) => Effect.Effect<void> }
>() {}
const getUser = Effect.gen(function* () {
const db = yield* Database
const logger = yield* Logger
yield* logger.log("fetching user")
return yield* db.query("select 1")
})
// getUser: Effect<string, never, Database | Logger>
// ^ requirements merged into R automatically — no manual intersection
The R channel accumulates Database | Logger on its own, and you can’t run the program until every requirement is provided by a Layer. That’s the safety property: because the typed Context requirements are gathered up and checked before the program can run, a missing dependency is a compile error at the edge, not a runtime surprise. The fp-ts Reader stack can express the same shape, but the wiring is yours to keep correct, and that’s the slip Effect’s accumulate-then-check requirements model closes.
Two different learning curves, not one wall
“Learning curve” is two separate walls, and which one you hit depends on which library you pick — neither competitor splits this out.
fp-ts’s wall is abstraction. To use it well you internalize higher-kinded type encoding, typeclasses (Functor, Monad, Monoid, Traversable), the pipe/flow style, and the zoo of pre-composed monads like ReaderTaskEither. The concepts are mathematical and transfer to other FP languages, but the payoff is gated behind category-theory vocabulary.
This connects to what I covered in runtime narrowing patterns.
Effect’s wall is the runtime model. The algebra is friendlier — generators (Effect.gen) read like async/await — but you must learn the Effect channel semantics, Layer and Context for DI, Fiber for concurrency, Scope for resource safety, and Schedule for retries. Each is a new primitive your whole team commits to.
A developer fluent in Haskell-style FP will find fp-ts familiar and Effect’s runtime novel; a developer fluent in Node services will find Effect’s gen approachable and fp-ts’s typeclasses alien. The “curve” depends on where you start.

The radar chart plots both libraries across ergonomics, error tracking, ecosystem, operational cost, learning curve, and lock-in. The shapes don’t nest — Effect bulges on built-in runtime features and ecosystem, fp-ts bulges on minimalism and low lock-in. There is no axis on which one strictly dominates, which is the whole point.
What you actually pay: bundle size
Effect’s bundle is larger because it ships a fiber runtime; fp-ts tree-shakes to near nothing because there’s no runtime to ship. The Effect docs concede the larger initial size and argue it “amortizes” as you use more features — true, but only if you actually use them.
The escape hatch is the Micro module. The Micro for Effect users guide describes it as a lightweight subset that deliberately drops Layer, Ref, Queue, and Deferred. It’s aimed at libraries that expose Promise-based APIs and want Effect’s ergonomics without dragging in the full runtime. The catch the docs are explicit about: import any major full-Effect module and the runtime gets pulled back in, erasing Micro’s savings.
The reasoning here builds on watching your performance budget.
These are relative orders of magnitude, not measured benchmarks: the figures come from the qualitative description in the Effect docs (the Micro guide on what the subset omits) plus the fact that fp-ts ships no runtime, so the only honest claim is directional — your real numbers depend on which modules you import and how aggressively your bundler tree-shakes.
Feature comparison — Effect vs fp-ts.
The comparison view above lines the three setups up side by side. The takeaway isn’t a single winning number — it’s that bundle size is a knob, not a verdict: fp-ts and Micro are close for small clients, and full Effect’s weight only pays off once you’re using concurrency, DI, and observability.
The 2026 reality check: the unified effect package
If you follow a 2023-era migration guide, the imports won’t resolve. Effect used to be split across @effect/io, @effect/data, and friends; that era is over. Today everything lives in the single effect package, and APIs were renamed in the consolidation.
Run a verbatim 2023 snippet against the current package and it fails at import:
// 2023-style code — does NOT resolve on the current `effect` package
import * as Effect from "@effect/io/Effect"
const fetchUser = Effect.tryCatchPromise(
() => fetch("/api/user").then((r) => r.json()),
(error) => new Error(String(error))
)
// TS / bundler: Cannot find module '@effect/io/Effect'
// 'tryCatchPromise' does not exist
Here’s what the example produces.
The terminal capture shows exactly that failure mode — the module path is gone and the old combinator name no longer exists. This is the single biggest trap for anyone learning from older tutorials: the concepts survived, the package layout did not.
The current equivalent uses the unified import and the renamed tryPromise:
// Current `effect` package
import { Effect } from "effect"
const fetchUser = Effect.tryPromise({
try: () => fetch("/api/user").then((r) => r.json()),
catch: (error) => new Error(String(error)),
})
Methodology and source check
This source check verified the comparison against the current official Effect documentation (the Effect vs fp-ts page, the expected-errors guide, and the Micro module guide), the fp-ts README, and the fp-ts/effect-ts migration discussion, reviewed on 2026-06-01.
The “overlap only at the typed-values layer” characterization is qualitative: it reflects that the shared API surface is limited to typed containers (an option type, an either/result type, pipeable composition), while Effect’s runtime features — fibers, Layer DI, scopes, schedules, metrics, tracing — have no fp-ts counterpart, as the official feature matrix and discussion #1852 both describe.
The dependency-injection comparison is framed on observable behavior rather than variance internals: in Effect, typed Context requirements accumulate into the R channel and must all be satisfied by a Layer before the program type-checks as runnable, whereas in fp-ts the environment intersection is composed and kept correct by hand.
Dimensions compared:
- Error tracking
- Dependency-injection type safety
- Learning curve
- Bundle size
- Ecosystem scope
- Lock-in
The code listings are written against the unified effect package and fp-ts v2 APIs; the inferred-type comments reflect documented behavior. Bundle figures are directional ranges, not a single measured byte count — actual size depends on which modules you import and how aggressively your bundler tree-shakes, so treat them as orders of magnitude rather than benchmarks.
The rubric: choose fp-ts, choose Effect, migrate, or stay
Tie the decision to the runtime-vs-algebra pivot, not to which name sounds newer.
- Choose fp-ts if you want composable typed values (
Either,Option,TaskEither) and typeclass abstractions inside an architecture you already control, with a minimal bundle and no runtime buy-in. Good fit for a library, a validation layer, or a team comfortable with FP vocabulary. - Choose Effect if you need the runtime: structured concurrency with interruption,
Layer-based DI that merges requirements safely, resource scoping, retries, and built-in tracing/metrics. Good fit for a whole application or service where theEffecttype can be the spine. - Migrate from fp-ts to Effect if you keep reaching for things fp-ts doesn’t ship — real cancellation, fiber concurrency, observability — and you’re prepared to budget it as a re-architecture, not a dependency swap. The maintainer’s own advice in discussion #1852 is the test: only if the benefits beat the cost.
- Stay on fp-ts if it already does what you need. It still receives maintenance releases, and there is no deadline forcing you off it.
The trap to avoid is adopting Effect “because it’s the successor.” That commits your codebase to a runtime mental model — fibers, Layer, Scope — that your team has to learn, and it’s a multi-month bet, not a version bump. Decide on the one question that actually separates them: do you need the runtime, or just the algebra? Answer that, and the rest of the choice follows.
I compared the options in migrating incrementally without breakage.
Is Effect a drop-in replacement for fp-ts?
No. Effect shares only the typed-values layer with fp-ts — an option type, an either/result type, and pipeable composition. Everything above that, including fibers, Layer dependency injection, and scopes, is a new runtime model. Moving across is a re-architecture you should budget like adopting a new framework, not a version bump or a mechanical find-and-replace.
Which has the smaller bundle, Effect or fp-ts?
fp-ts is smaller because it ships no runtime and tree-shakes to almost nothing when you import only Either and pipe. Full Effect bundles a fiber runtime, so it is larger. Effect’s Micro module sits in between as a lighter subset, but importing any major full-Effect module pulls the runtime back in and erases those savings.
Does fp-ts still receive updates after the Effect merger?
Yes. fp-ts v2 is not abandoned and there is no deadline forcing you off it. Its releases page shows v2.16.x maintenance tags landing into late 2024, and the maintainer’s guidance is to migrate only when the benefits outweigh the cost. If fp-ts already does what you need, staying put is a legitimate choice.
How does error tracking differ between Effect and fp-ts?
Both type the error channel, so that part is shared. The difference is recovery: Effect’s catchTag subtracts a handled variant from the error union automatically, so the compiler keeps an accurate ledger of what is still unhandled. In fp-ts you narrow on _tag by hand and re-emit the rest, and the inferred error type does not shrink.
Further reading
- Effect documentation: Effect vs fp-ts comparison — the official feature matrix and the “successor to fp-ts v2” framing.
- fp-ts and effect-ts (gcanti/fp-ts Discussion #1852) — the migration cost-benefit guidance and community pushback on the “v3” label.
- Effect: Expected Errors — the E channel,
Data.TaggedError, andcatchTagunion subtraction. - Effect: Micro for Effect Users — the lightweight module and what it omits.
- fp-ts README — the merger announcement and current v2 maintenance status.
- A bright future for Effect — the announcement that there will be no separate fp-ts v3.
