v4 Changes

Effect v4 introduces breaking changes across the core runtime, service definitions, error handling, module structure, and naming conventions. This page covers the major changes and how to migrate.

Comparison Table

Areav3v4
Effect subtypingRef, Deferred, Fiber extend EffectYieldable trait; use explicit methods
ServicesContext.Tag, Effect.Tag, Effect.ServiceContext.Service
Catch allEffect.catchAllEffect.catch
Catch all causeEffect.catchAllCauseEffect.catchCause
Catch someEffect.catchSomeEffect.catchFilter
Eithereffect/Eithereffect/Result
STM refsTRef, TMap, TSetTxRef, TxHashMap, TxHashSet
Fiber keep-aliveRequires runMain timer hackBuilt into core runtime
Layer memoizationPer Effect.provide callShared across Effect.provide calls
Platform packages@effect/platform, @effect/cli, etc.effect/unstable/*
Schema filterSchema.filter(predicate)Schema.check(Schema.makeFilter(...))
Schema transformSchema.transform(from, to, opts)from.pipe(Schema.decodeTo(to, ...))
ParseResulteffect/ParseResulteffect/SchemaIssue, effect/SchemaParser
TestClockeffect/TestClockeffect/testing/TestClock
FiberRefeffect/FiberRefeffect/References

Yieldable Trait

v3 made many types structural subtypes of Effect. You could pass a Ref to Effect.map and it would silently read the ref’s value. This caused subtle bugs.

v4 introduces the Yieldable trait: a narrower contract that allows yield* in generators but does not make the type assignable to Effect.

// yield* still works with Yieldable types
const program = Effect.gen(function*() {
  const value = yield* Option.some(42)  // Option is Yieldable
  return value
})

But combinators require explicit conversion:

// v3: Option<number> is assignable to Effect<number, NoSuchElementError>
const program = Effect.map(Option.some(42), (n) => n + 1)

// v4: use .asEffect() or a generator
const program = Effect.map(Option.some(42).asEffect(), (n) => n + 1)
const program2 = Effect.gen(function*() {
  const n = yield* Option.some(42)
  return n + 1
})

Types No Longer Subtypes of Effect

Typev3 patternv4 replacement
Refyield* refyield* Ref.get(ref)
Deferredyield* deferredyield* Deferred.await(deferred)
Fiberyield* fiberyield* Fiber.join(fiber)

Gotcha: If you have yield* someRef in v3 code, this will fail to compile in v4. Replace with yield* Ref.get(someRef). The same applies to Deferred and Fiber.

Context.Service Pattern

v4 replaces all service definition APIs with Context.Service.

Class-Based Services

// v3
class Database extends Context.Tag("Database")<Database, {
  query(sql: string): Effect.Effect<Array<unknown>>
}>() {}

// v4
class Database extends Context.Service<Database, {
  query(sql: string): Effect.Effect<Array<unknown>>
}>()("myapp/Database") {}

Note the argument order: in v3, the identifier string comes first. In v4, the type parameters come first, then the identifier string.

Effectful Constructors with make

v3’s Effect.Service auto-generated a .Default layer. v4’s Context.Service with make stores the constructor but does not auto-generate a layer. You define layers explicitly.

// v4
class Logger extends Context.Service<Logger>()("myapp/Logger", {
  make: Effect.gen(function*() {
    const config = yield* Config
    return { log: (msg: string) => Effect.log(`[${config.prefix}] ${msg}`) }
  })
}) {
  static readonly layer = Layer.effect(this, this.make).pipe(
    Layer.provide(Config.layer)
  )
}

Accessors Removed

v3’s Effect.Tag provided static accessor proxies. v4 removes them. Use yield* or Service.use:

// v3: static proxy access
const program = Notifications.notify("hello")

// v4: yield the service
const program = Effect.gen(function*() {
  const notifications = yield* Notifications
  yield* notifications.notify("hello")
})

// or use:
const program = Notifications.use((n) => n.notify("hello"))

Tip: Prefer yield* over use. It makes dependencies explicit at the call site and keeps service access co-located with your effect logic. use can accidentally hide service dependencies in return values.

Quick Reference

v3v4
Context.GenericTag<T>(id)Context.Service<T>(id)
Context.Tag(id)<Self, Shape>()Context.Service<Self, Shape>()(id)
Effect.Tag(id)<Self, Shape>()Context.Service<Self, Shape>()(id)
Effect.Service<Self>()(id, opts)Context.Service<Self>()(id, { make })

catch* Renamings

v3v4
Effect.catchAllEffect.catch
Effect.catchAllCauseEffect.catchCause
Effect.catchAllDefectEffect.catchDefect
Effect.catchSomeEffect.catchFilter
Effect.catchSomeCauseEffect.catchCauseFilter
Effect.catchSomeDefectRemoved
Effect.catchTagEffect.catchTag (unchanged)
Effect.catchTagsEffect.catchTags (unchanged)
Effect.catchIfEffect.catchIf (unchanged)

catchSome to catchFilter

v3’s catchSome took a function returning Option<Effect>. v4’s catchFilter uses the Filter module.

// v3
Effect.fail(42).pipe(
  Effect.catchSome((error) =>
    error === 42 ? Option.some(Effect.succeed("caught")) : Option.none()
  )
)

// v4
import { Effect, Filter } from "effect"

Effect.fail(42).pipe(
  Effect.catchFilter(
    Filter.fromPredicate((error: number) => error === 42),
    (error) => Effect.succeed("caught")
  )
)

New in v4

  • Effect.catchReason(errorTag, reasonTag, handler) - catches a specific reason within a tagged error
  • Effect.catchReasons(errorTag, cases) - handles multiple reason tags
  • Effect.catchEager(handler) - optimization variant for synchronous recovery

Automatic Fiber Keep-Alive

v3’s core runtime did not keep the process alive while fibers were suspended on async operations. You needed runMain from @effect/platform-node to install a timer.

v4 builds keep-alive into the core runtime. The fiber runtime automatically manages a reference-counted keep-alive timer.

// v4: works without runMain
import { Deferred, Effect } from "effect"

const program = Effect.gen(function*() {
  const deferred = yield* Deferred.make<string>()
  yield* Deferred.await(deferred)
})

Effect.runPromise(program) // process stays alive

Tip: runMain is still recommended for production. It provides signal handling (SIGINT/SIGTERM), exit code management, and error reporting. The keep-alive change just means the core runtime no longer exits prematurely.

Layer Auto-Memoization

v3 memoized layers within a single Effect.provide call, but not across separate calls. Two Effect.provide calls with overlapping layers would build them twice.

v4 shares the MemoMap between Effect.provide calls by default:

const main = program.pipe(
  Effect.provide(MyServiceLayer),
  Effect.provide(MyServiceLayer)  // v3: builds twice. v4: builds once
)

Opting Out

Use Layer.fresh to bypass the shared cache, or Effect.provide(layer, { local: true }) for an entire isolated layer subtree:

const main = program.pipe(
  Effect.provide(MyServiceLayer),
  Effect.provide(Layer.fresh(MyServiceLayer))  // always builds fresh
)

Gotcha: Even though v4 auto-memoizes across provide calls, composing layers before providing is still the recommended pattern. Auto-memoization is a safety net, not a substitute for explicit dependency wiring.

Consolidated Packages

v4 merges previously separate packages into effect/unstable/*:

v3 packagev4 location
@effect/platform (HTTP, FileSystem, etc.)effect/unstable/http, effect/FileSystem, effect/Path
@effect/platform (HttpApi)effect/unstable/httpapi
@effect/clieffect/unstable/cli
@effect/clustereffect/unstable/cluster
@effect/aieffect/unstable/ai
@effect/sqleffect/unstable/sql
@effect/rpceffect/unstable/rpc
@effect/workfloweffect/unstable/workflow
@effect/opentelemetry/Otlp*effect/unstable/observability/Otlp*

Warning: unstable/ modules may break in minor releases. Stabilized modules move to effect/* top-level. Import specific modules to keep bundles small: import { HttpApi } from "effect/unstable/httpapi".

Either to Result

v4 renames Either to Result across the board:

// v3
import { Either } from "effect"
const right = Either.right(42)
const isRight = Either.isRight(value)

// v4
import { Result } from "effect"
const right = Result.right(42)
const isRight = Result.isSuccess(value)

The Either import path effect/Either is now effect/Result.

Tx* Prefix for STM Types

Software transactional memory types are renamed with a Tx prefix:

v3v4
TRefTxRef
TMapTxHashMap
TSetTxHashSet
TQueueTxQueue
TPubSubTxPubSub
TSemaphoreTxSemaphore
TDeferredTxDeferred
TSubscriptionRefTxSubscriptionRef