# Migrating from v3

Effect v4 keeps the programming model you already know — `Effect`, `Layer`,
`Schema`, `Stream` are conceptually unchanged — but it reorganises packages,
renames a family of combinators, and tightens a few type-level contracts. This
page summarises the changes you are most likely to hit and shows the v4 form
side by side with the v3 form.
**Note:** Effect v4 is in beta. APIs may still shift between beta releases; treat the
  rename maps here as a snapshot of the current beta.

## A service, end to end

Most v3 code that defines a service, builds a layer, and runs a program needs
only mechanical changes. Here is the same program written in both versions, so
you can see the shape of the migration at a glance.

```ts
import { Context, Effect, Layer } from "effect"

// Services are classes extending Context.Service<Self, Shape>()(id).
// The type parameters come first, the identifier is passed afterwards.
class Database extends Context.Service<Database, {
  readonly query: (sql: string) => Effect.Effect<ReadonlyArray<unknown>>
}>()("app/Database") {}

// A layer is built explicitly with Layer.effect — there is no auto-generated
// `.Default`. The convention is to name the primary layer `layer`.
const DatabaseLayer = Layer.effect(
  Database,
  Effect.succeed({
    query: (sql) => Effect.succeed([{ sql }])
  })
)

// Access the service with yield* in a generator — dependencies stay explicit.
const program = Effect.gen(function*() {
  const db = yield* Database
  return yield* db.query("SELECT 1")
})

program.pipe(Effect.provide(DatabaseLayer), Effect.runPromise)
```
  ```ts
import { Effect } from "effect"

// Services were defined with Effect.Service and auto-generated a `.Default`
// layer from the `effect` constructor.
class Database extends Effect.Service<Database>()("app/Database", {
  effect: Effect.succeed({
    query: (sql: string) => Effect.succeed([{ sql }])
  })
}) {}

const program = Effect.gen(function*() {
  const db = yield* Database
  return yield* db.query("SELECT 1")
})

program.pipe(Effect.provide(Database.Default), Effect.runPromise)
```
  The accessing code (`yield* Database`) is identical. What changed is how the
service is *defined* and how its *layer* is built — covered next.

## Packaging and imports

Two structural changes affect every project, regardless of which APIs you use.

**Single version number.** All Effect ecosystem packages now share one version
and are released together. If you depend on `effect@4.0.0-beta.0`, the matching
SQL driver is `@effect/sql-pg@4.0.0-beta.0`. In v3 these were versioned
independently, which made compatibility hard to track.

**Package consolidation.** Functionality from `@effect/platform`,
`@effect/rpc`, `@effect/cluster`, and others now lives inside the core `effect`
package. Stable modules are imported from `effect`; the ported-but-evolving ones
live under `effect/unstable/<area>`:

```ts
// Stable core — top-level barrel
import { Effect, Layer, Schema, Stream } from "effect"

// Unstable modules — versioned more loosely, may change in minor releases
import { HttpClient } from "effect/unstable/http"
import { Command } from "effect/unstable/cli"
import { LanguageModel } from "effect/unstable/ai"
```

Packages that stay separate are platform-, driver-, or provider-specific:
`@effect/platform-node`, `@effect/sql-pg`, `@effect/ai-openai`,
`@effect/opentelemetry`, `@effect/vitest`. Bump them to matching v4 versions.
**Tip:** Modules under `effect/unstable/*` graduate to the top-level `effect/*`
  namespace as they stabilise. Until then they follow looser versioning, so
  pin your dependency range accordingly.

## Defining services: `Context.Service`

In v3, services could be defined with `Context.Tag`, `Context.GenericTag`,
`Effect.Tag`, or `Effect.Service`. In v4, all of these collapse into a single
constructor: **`Context.Service`**.

```ts
import { Context, Effect, Layer } from "effect"

class Logger extends Context.Service<Logger>()("app/Logger", {
  // `make` stores the constructor effect on the class but does NOT
  // auto-generate a layer (unlike v3's Effect.Service).
  make: Effect.succeed({
    log: (msg: string) => Effect.log(msg)
  })
}) {
  // Build the layer yourself. v4 names it `layer`, not `Default`/`Live`.
  static readonly layer = Layer.effect(this, this.make)
}
```

The most important behavioural change: the `dependencies` option is gone. Wire
a service's dependencies with `Layer.provide` when you build its layer:

```ts
static readonly layer = Layer.effect(this, this.make).pipe(
  Layer.provide(Config.layer)
)
```

**References (services with defaults)** are still `Context.Reference`, but the
function form is used in v4:

```ts
import { Context } from "effect"

const LogLevel = Context.Reference<"info" | "warn" | "error">("app/LogLevel", {
  defaultValue: () => "info" as const
})
```

| v3 | v4 |
| --- | --- |
| `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, { effect, dependencies })` | `Context.Service<Self>()(id, { make })` + explicit `Layer` |
| `tag.Default` / `tag.Live` | `tag.layer` (named by convention) |

## `FiberRef` is gone — use `Context.Reference`

`FiberRef`, `FiberRefs`, `FiberRefsPatch`, and `Differ` were removed. Fiber-local
state is now a `Context.Reference` — the same mechanism as services with
defaults. The built-in refs moved onto the `References` module:

```ts
import { Effect, References } from "effect"

// Read a built-in reference by yielding it directly.
const program = Effect.gen(function*() {
  const level = yield* References.CurrentLogLevel
  return level
})

// Scope a value for the duration of an effect with provideService —
// this replaces v3's Effect.locally / FiberRef.set.
const debugged = program.pipe(
  Effect.provideService(References.CurrentLogLevel, "Debug")
)
```

| v3 | v4 |
| --- | --- |
| `FiberRef.get(FiberRef.currentLogLevel)` | `yield* References.CurrentLogLevel` |
| `Effect.locally(eff, ref, value)` | `Effect.provideService(eff, Reference, value)` |
| `FiberRef.currentConcurrency` | `References.CurrentConcurrency` |
| `FiberRef.currentScheduler` | `References.Scheduler` |

## Error handling: `catchAll*` → `catch*`

The `catch` family was shortened: `catchAll*` drops the `All`, and the
`catchSome*` family is replaced by filter-based combinators.

```ts
import { Effect } from "effect"

const recovered = Effect.fail("boom").pipe(
  // v3: Effect.catchAll
  Effect.catch((error) => Effect.succeed(`recovered: ${error}`))
)
```

| v3 | v4 |
| --- | --- |
| `Effect.catchAll` | `Effect.catch` |
| `Effect.catchAllCause` | `Effect.catchCause` |
| `Effect.catchAllDefect` | `Effect.catchDefect` |
| `Effect.catchSome` | `Effect.catchFilter` |
| `Effect.catchSomeCause` | `Effect.catchCauseFilter` |
| `Effect.catchTag` / `catchTags` / `catchIf` | unchanged |

v4 also adds `Effect.catchReason` / `catchReasons` for handling a nested
`reason` within a tagged error without removing the parent error from the error
channel — useful for structured errors such as an `AiError` carrying a
`RateLimitError` reason.

## `Either` → `Result`

`Either` has been renamed to `Result`. Same shape, new names for the
constructors:

```ts
import { Result } from "effect"

const ok = Result.succeed(42) // was Either.right
const no = Result.fail("nope") // was Either.left
```

Several extractors now return a `Result` where v3 returned an `Option`. For
example, `Cause.findError` returns a `Result`; use `Cause.findErrorOption` for
the `Option`-based variant.

## Forking: clearer names, options object

The `fork*` family was renamed for clarity, and every variant now takes an
options object (`startImmediately`, `uninterruptible`).

```ts
import { Effect, Fiber } from "effect"

const program = Effect.gen(function*() {
  // v3: Effect.fork — a child fiber tied to the parent's lifetime.
  const fiber = yield* Effect.forkChild(longRunningTask)
  // Fiber is no longer an Effect — join it explicitly (see below).
  return yield* Fiber.join(fiber)
})

declare const longRunningTask: Effect.Effect<number>
```

| v3 | v4 |
| --- | --- |
| `Effect.fork` | `Effect.forkChild` |
| `Effect.forkDaemon` | `Effect.forkDetach` |
| `Effect.forkScoped` / `forkIn` | unchanged |
| `Effect.forkAll` / `forkWithErrorHandler` | removed (fork individually) |

## Yieldable: explicit conversion to `Effect`

In v3 many types (`Ref`, `Deferred`, `Fiber`, `Option`, `Either`, …) were
structural subtypes of `Effect` and could be passed anywhere an `Effect` was
expected. This caused subtle bugs — accidentally mapping over a `Ref` instead of
its value, for instance.

v4 replaces subtyping with the narrower **`Yieldable`** trait: a type can be
`yield*`-ed in a generator but is *not* assignable to `Effect`. So `yield*`
still works as before, while passing the value to a combinator now requires an
explicit conversion.

```ts
import { Effect, Option, Ref } from "effect"

const program = Effect.gen(function*() {
  // yield* still works on Yieldable values, exactly like v3.
  const a = yield* Option.some(42)

  // Ref/Deferred/Fiber are plain values now — read them through their module.
  const ref = yield* Ref.make(0)
  const b = yield* Ref.get(ref) // v3: `yield* ref`
  return a + b
})

// Outside a generator, convert a Yieldable explicitly with Effect.fromOption
// (or Effect.fromResult for a Result) before passing it to a combinator.
const mapped = Effect.map(Effect.fromOption(Option.some(42)), (n) => n + 1)
```

| v3 (was an Effect) | v4 (read via module) |
| --- | --- |
| `yield* ref` | `yield* Ref.get(ref)` |
| `yield* deferred` | `yield* Deferred.await(deferred)` |
| `yield* fiber` | `yield* Fiber.join(fiber)` |

## `Cause` is flat

`Cause` is no longer a recursive tree of `Sequential` / `Parallel` nodes. It is
now a flat wrapper over an array of `Reason` values, where a reason is one of
`Fail`, `Die`, or `Interrupt`:

```ts
import { Cause } from "effect"

const summarise = (cause: Cause.Cause<string>) => {
  // Iterate the flat reasons array instead of recursing a tree.
  for (const reason of cause.reasons) {
    switch (reason._tag) {
      case "Fail":
        return reason.error
      case "Die":
        return reason.defect
      case "Interrupt":
        return reason.fiberId
    }
  }
}
```

The `*Exception` classes were renamed to `*Error` (e.g.
`NoSuchElementException` → `Cause.NoSuchElementError`,
`TimeoutException` → `Cause.TimeoutError`).

## Other notable renames

A few changes you will run into while porting:

| v3 | v4 |
| --- | --- |
| `Effect.async` | `Effect.callback` |
| `Effect.either` | `Effect.result` |
| `Effect.zipRight` / `zipLeft` | `Effect.andThen` / `Effect.tap` |
| `Layer.scoped` | `Layer.effect` |
| `Scope.extend` | `Scope.provide` |
| `Runtime<R>` / `Runtime.runFork(runtime)` | removed — use `Context<R>` + `Effect.runForkWith(services)` |
| `Stream.fromChunk` / `fromChunks` | `Stream.fromArray` / `fromArrays` |
| `Stream.async` | `Stream.callback` |
| `effect/JSONSchema` | `effect/JsonSchema` |

`Equal.equals` also now uses **structural** equality by default for plain
objects, arrays, `Map`s, `Set`s, `Date`s, and `RegExp`s — no `structuralRegion`
needed. Opt back into reference equality with `Equal.byReference`.

## Verifying the change

The fiber runtime was rewritten for lower memory overhead and faster execution,
and the core package tree-shakes aggressively — a minimal Effect program bundles
to roughly 6.3 KB minified + gzipped, or ~15 KB with Schema. After porting, your
existing test suite is the best signal that behaviour is preserved; see
[Testing](https://effect.plants.sh/testing/) for the v4 testing utilities.

For the complete machine-generated import and API rename maps, see the
`migration/` directory in the `effect-smol` repository.