Skip to main content
Effect v3 introduces several breaking changes to improve type safety, consistency, and developer experience.

Installation

Update your dependencies to the latest versions:
pnpm update effect@latest

Breaking Changes

Module Reorganization

Several modules have been reorganized for better consistency.
import * as Effect from "@effect/io/Effect"
import * as Layer from "@effect/io/Layer"
import * as Runtime from "@effect/io/Runtime"
import * as Stream from "@effect/stream/Stream"
All core modules are now exported from the single effect package.

Service Definition

Service definition has been simplified with a new Context.Tag API.
import { Context } from "effect"

interface Database {
  readonly query: (sql: string) => Effect.Effect<unknown, Error>
}

const Database = Context.Tag<Database>("@app/Database")
The new class-based approach provides better type inference and IDE support. Update all service definitions.

Layer Construction

const DatabaseLive = Layer.succeed(
  Database,
  {
    query: (sql) => Effect.succeed(/* ... */)
  }
)

Effect.gen Changes

The generator API remains similar but has improved type inference.
const program = Effect.gen(function* ($) {
  const value = yield* $(Effect.succeed(42))
  return value
})
The $ adapter is no longer needed. You can yield Effects directly.

Error Channel Type

Error types are now more strictly enforced.
const effect: Effect.Effect<never, unknown, number> =
  Effect.fail("error")
Always use structured error types instead of strings or unknown. This improves type safety and error handling.

Scope and Resource Management

Effect.acquireUseRelease(
  acquire,
  use,
  release
)

Config Changes

Configuration has been streamlined.
import * as Config from "@effect/io/Config"

const port = Config.number("PORT").pipe(
  Config.withDefault(3000)
)

Schedule Changes

Schedule combinators have been refined.
import * as Schedule from "@effect/io/Schedule"

const schedule = Schedule.exponential("100 millis").pipe(
  Schedule.either(Schedule.spaced("1 second"))
)
Duration helpers replace string-based duration parsing for better type safety.

Stream Changes

import * as Stream from "@effect/stream/Stream"

const stream = Stream.fromIterable([1, 2, 3])

Runtime Changes

import * as Runtime from "@effect/io/Runtime"

const runtime = Runtime.defaultRuntime

Runtime.run(runtime, effect)

New Features in v3

Refined Concurrency Control

Better control over concurrent operations.
const results = yield* Effect.forEach(
  items,
  processItem,
  { concurrency: 5 } // or "unbounded", "inherit"
)

Improved Interruption

More granular interruption control.
const program = Effect.uninterruptible(
  criticalSection
).pipe(
  Effect.zipRight(
    Effect.interruptible(interruptibleSection)
  )
)

Better Type Inference

Type inference has been significantly improved throughout.
const result = yield* Effect.all({
  user: fetchUser(),
  settings: fetchSettings(),
  // Return type is automatically inferred
})
// result: { user: User, settings: Settings }

Enhanced Error Handling

New error handling combinators.
const program = effect.pipe(
  Effect.catchTag("NetworkError", error =>
    Effect.log("Network failed").pipe(
      Effect.zipRight(Effect.fail(error))
    )
  ),
  Effect.catchTag("ValidationError", error =>
    Effect.succeed(defaultValue)
  )
)

Migration Strategy

Step 1: Update Imports

Replace all imports with the new package structure.
# Find and replace in your codebase
@effect/io/Effect -> effect
@effect/stream/Stream -> effect

Step 2: Update Service Definitions

Convert all Context.Tag usages to the new class-based API.
// Old pattern
const Service = Context.Tag<ServiceInterface>("Service")

// New pattern
class Service extends Context.Tag("Service")<
  Service,
  ServiceInterface
>() {}

Step 3: Update Effect.gen

Remove the $ adapter from all generator functions.
# Find: yield* $(effect)
# Replace: yield* effect

Step 4: Type Your Errors

Define proper error types for all failure cases.
class ValidationError {
  readonly _tag = "ValidationError"
  constructor(readonly errors: string[]) {}
}

class NetworkError {
  readonly _tag = "NetworkError"
  constructor(readonly cause: unknown) {}
}

type AppError = ValidationError | NetworkError

Step 5: Run Tests

Ensure all tests pass after migration.
pnpm test

Step 6: Update Type Annotations

Review and update type annotations to leverage improved inference.
const program: Effect.Effect<never, Error, Result> = Effect.gen(function* () {
  // ...
})

Common Migration Issues

Issue: Import Errors

Problem: Cannot find module @effect/io/Effect Solution: Update to new import structure:
import { Effect } from "effect"

Issue: Type Errors with Context.Tag

Problem: Context.Tag no longer accepts interface directly Solution: Use class-based syntax:
class MyService extends Context.Tag("MyService")<
  MyService,
  ServiceInterface
>() {}

Issue: Generator Function Errors

Problem: Type errors with yield* $(effect) Solution: Remove the $ adapter:
const value = yield* effect // No $ needed

Testing After Migration

Update your test setup to use the new APIs:
import { it } from "@effect/vitest"
import { Effect, Layer } from "effect"

it.effect("should process user", () =>
  Effect.gen(function* () {
    const service = yield* MyService
    const result = yield* service.processUser("123")
    assert.strictEqual(result.id, "123")
  }).pipe(
    Effect.provide(MyServiceTest)
  )
)
Use it.effect from @effect/vitest for testing Effect programs. It handles the Effect runtime automatically.

Performance Improvements

v3 includes several performance optimizations:
  • Reduced memory allocations
  • Faster fiber scheduling
  • Improved concurrent execution
  • Better tree-shaking support

Next Steps

Getting Help

If you encounter issues during migration:

Build docs developers (and LLMs) love