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.
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.
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: