v3 → v4 Migration Field Guide
Who needs this, philosophy shifts, package consolidation and unstable paths, one-version rule, rewritten runtime and bundle size, the load-bearing rename table, services collapse, yieldable narrowing, new APIs, import maps, and an annotated porting diff — with links to MIGRATION.md and migration topic guides.
This chapter is for two audiences:
- You have a v3 codebase and are planning the upgrade to
4.0.0-rc.*. - You have no v3 codebase but you read blog posts, StackOverflow answers, or older library docs that speak v3 — you need to translate them on sight.
v4 is not a codemod-and-done release. The phrasing is blunt in MIGRATION.md itself: the core programming model (Effect, Layer, Schema, Stream) is unchanged, but how packages are organized, versioned, and imported has changed significantly and many combinators were renamed. Plan for a deliberate, chapter-by-chapter migration rather than a flag day.
Philosophy shifts in one page
Section titled “Philosophy shifts in one page”| Theme | v3 | v4 | Why you care |
|---|---|---|---|
| Packages | effect + @effect/platform, @effect/rpc, @effect/cluster, … |
Consolidated into effect; platform drivers stay separate (@effect/platform-node, @effect/sql-*) |
One install line per platform; fewer peerDeps |
| Unstable APIs | mixed into their packages | Explicit effect/unstable/* import paths (http, httpapi, sql, ai, observability, schema, sql, …) |
Minor releases may break unstable — the path tells you |
| Versioning | independent per package (effect@3.x, @effect/platform@0.x) |
One version for the whole ecosystem (effect@4.0.0-rc.112 pairs with @effect/platform-node@4.0.0-rc.112) |
No more “which platform matches which core?” |
| Runtime | rewritten? No — large, bit-flag RuntimeFlags + Runtime<R> bundle you carried |
Rewritten for memory/speed. No flags. Context<R> is the runtime; behavior via References. Minimal bundle ≈ 6 KB gz (~15 KB with Schema). Micro deleted — core is already micro. |
Simpler mental model, faster startup, less to import |
| Tree-shaking | partial | Aggressive — minimal program ≈ 6.3 KB min+gz | Matters for edge/serverless |
The load-bearing rename table
Section titled “The load-bearing rename table”This is the diff you will retype most often. Roughly ordered by frequency in a typical codebase.
| v3 | v4 | Detail |
|---|---|---|
Either<L,R> |
Result<E, A>, error-first (Result<E, A> not Result<A, E>) |
Right → Success, Left → Failure, Either.Do → Result.Do, getLefts → getFailures, array.liftEither → liftResult |
Effect.catchAll(handler) |
Effect.catch(handler) |
shortened — typed failure E |
Effect.catchAllCause(handler) |
Effect.catchCause(handler) |
full Cause<E> |
Effect.catchAllDefect(handler) |
Effect.catchDefect(handler) |
defects only |
Effect.catchSome(pf) |
Effect.catchFilter(Filter, handler) |
Option-returning partial function → Filter module |
Effect.catchSomeCause(pf) |
Effect.catchCauseFilter(Filter, handler) |
filtered cause |
Effect.fork(effect) |
Effect.forkChild(effect) |
structured child; dies with parent |
Effect.forkDaemon(effect) |
Effect.forkDetach(effect) |
global-scope fire-and-forget |
Effect.forkAll(effects) |
Effect.forEach(effects, Effect.forkChild) + Fiber.joinAll |
or a higher concurrency combinator |
Effect.disconnect(effect) |
Effect.forkDetach(…) + await decision |
explicit detachment |
Effect.daemonChildren |
Effect.awaitAllChildren |
structured child waiting |
FiberRef / FiberRefs |
Context.Reference in References module |
per-fiber state is a defaulted service key; only Effect.provideService mutation, lexically scoped |
Runtime<R> |
Context<R> is the runtime |
Removed; services + references are the runtime bundle. Use Effect.context + Effect.run*With. RuntimeFlags deleted — behavior via References |
Scope.extend |
Scope.provide |
renamed; same data-first + curried forms |
Layer.scoped(service, acquire) |
Layer.effect(service, acquire) |
Merged — Layer.effect now supplies and excludes Scope automatically (Exclude<R, Scope>). Same for scopedContext → effectContext, scopedDiscard → effectDiscard |
Equal.Data / Data.struct / array / tuple / case |
Structural equality by default | Data.Class remains for class syntax; plain objects/arrays/Maps/Sets compare structurally without wrapping. Data.case/struct/tuple/array no longer needed. |
Effect.all / Effect.zip |
retained | no rename; Either → Result param flip is the gotcha inside doubles |
STM, TRef, TQueue, TMap, … |
TxRef, TxQueue, TxHashMap, TxHashSet, TxDeferred, TxSemaphore, TxSubscriptionRef, TxPriorityQueue, TxPubSub, TxReentrantLock |
prefix Tx (transaction) |
Effect.supervised |
FiberSet |
track explicitly forked fibers in a scoped FiberSet |
pipe |
Still exists | But this course uses flow occasionally for point-free pipe chains — style note, not API removal |
Effect.either |
Effect.result |
materializes Result<E,A> |
Channel.catchAll / catchAllCause |
Channel.catch / catchCause |
same rename as Effect |
Layer.catchAll / catchAllCause |
Layer.catch / catchCause |
same |
Layer.setScheduler |
Layer.succeed(Scheduler.Scheduler, scheduler) |
scheduler is now a Context.Reference |
Other error handling that stayed:
| v3 | v4 |
|---|---|
Effect.catchTag |
unchanged (now widens to multiple tags) |
Effect.catchTags |
unchanged |
Effect.catchIf |
unchanged |
Effect.catchSomeDefect |
removed — branch explicitly with Effect.catchDefect and re-die unhandled defects |
Services collapse — four ways became one
Section titled “Services collapse — four ways became one”v3 had four service key shapes; v4 has one, plus a sibling for defaults:
| v3 | v4 |
|---|---|
Context.Tag |
Context.Service |
Context.GenericTag |
Context.Service |
Effect.Tag |
Context.Service |
Effect.Service |
Context.Service |
Context.TagUnify etc. |
deleted |
| Per-service variance | Context.Service<T> (single arg) + Context.Service<Self, Shape>(id) class form |
Definition — flipped identifier order:
// v3class UserRepo extends Context.Tag("app/UserRepo")<UserRepo, { findById(id): Effect<User> }>() {}// or GenericTag / Effect.Service variants
// v4 — identifier is the second call argument; shape optionalexport class UserRepo extends Context.Service<UserRepo, { findById(id: string): Effect.Effect<User, NotFound>}>()("app/UserRepo") {}Context.Reference adds a mandatory default for fiber-local state:
export class CurrentTenant extends Context.Reference<CurrentTenant>()("app/CurrentTenant", { defaultValue: () => "public"}) {}FiberRef.make(initial) → Context.Reference(id, { defaultValue: () => initial }). Effect.locally(effect, ref, value) → Effect.provideService(effect, ref, value). FiberRef.get/set → yield* ref read; no imperative write — provisioning scopes value lexically.
Yieldable narrowing — Ref, Deferred, Fiber, Queue are no longer effects
Section titled “Yieldable narrowing — Ref, Deferred, Fiber, Queue are no longer effects”In v3 those types were structurally assignable to Effect, so yield* ref was legal — Effect.all([refA, refB]) accidentally read refs instead of pairing handles. The contract split:
| Type | v3 yield* |
v4 yield*? |
Correct accessor |
|---|---|---|---|
Effect<A,E,R> |
yes | yes | — |
Option<T> |
yes | yes (None fails with NoSuchElementError) |
— |
Result<E,A> |
n/a (Either) |
yes | — |
Config<T> |
yes | yes | — |
Context.Service |
yes (via Tag) | yes (Context.Key) |
— |
Ref<A> |
yes | no | yield* Ref.get(ref) |
Deferred<A,E> |
yes | no | yield* Deferred.await(d) |
Fiber<A,E> |
yes | no | yield* Fiber.join(fiber) |
Queue<A> |
yes | no | yield* Queue.take(q) |
SynchronizedRef, SubscriptionRef, FiberMap, … |
yes | no | explicit method |
Rule: Yieldable ≠ assignable. If you hold a handle, you hold a plain value. To run the operation, call the accessor. This turned a class of silent v3 bugs into compile errors.
// v3 — compiles, wrongconst wrong = Effect.all([refA, refB])
// v4 — type error, fix:const right = Effect.gen(function* () { const [a, b] = yield* Effect.all([Ref.get(refA), Ref.get(refB)]) return [a, b]})New APIs worth adopting immediately
Section titled “New APIs worth adopting immediately”| API | Replaces / adds | One-liner |
|---|---|---|
Effect.catchFilter(Filter, handler) |
catchSome |
Filtered recovery without Option |
Effect.catchReason(tag, reasonTag, handler) |
new | Peel a reason variant inside a tagged error without losing the parent wrapper |
Effect.catchReasons / catchTags extensions |
new | Multi-tag/ multi-reason in one call |
Filter module |
new for catchFilter |
Filter.fromPredicate, Filter.tag("Err"), combinators |
Reason-errors |
new error architecture | Schema.TaggedError carrying { reason }, typed catchReason support |
Tx* renames |
STM renames | TxRef, TxQueue, TxHashMap, … |
| Keep-alive | built into core | No manual hold — Layer.launch + fibers keep process open until interrupted |
Schedule.min / Schedule.max |
Schedule.union / intersect / intervals |
max = intersect (slower wins), min = union (faster wins); ScheduleIntervals / ScheduleInterval deleted |
Schedule.modifyDelay((meta) => Duration) |
new clamping | Schedule.modifyDelay(s, ({ delay }) => Duration.min(delay, "10 seconds")) |
Effect.withTracerEnabled(false) |
new | Locally disable span creation; complement to TracerEnabled reference |
Layer.withSpan("init") |
new | Trace layer construction spans |
Stream methods |
Stream.groupByKey(f); GroupBy.evaluate |
Ch. piping differences — see migration/v3-to-v4.md under Stream |
Cache / ScopedCache |
TestClock.adjust helpers unchanged |
But invalidation now uses Ref semantics; see migration/* |
Import map — highlights
Section titled “Import map — highlights”The exhaustive map is migration/v3-to-v4.md “Import Map”. These are the imports you will fix first:
| v3 import | v4 import |
|---|---|
effect/Either |
effect/Result (and Either → Result, Left → Failure) |
@effect/platform/FileSystem |
effect/FileSystem |
@effect/platform/Path |
effect/Path |
@effect/platform/Terminal |
effect/Terminal |
@effect/platform/HttpClient |
effect/unstable/http/HttpClient |
@effect/platform/HttpRouter |
effect/unstable/http/HttpRouter |
@effect/platform/HttpApi |
effect/unstable/httpapi/HttpApi |
@effect/platform/HttpApiBuilder |
effect/unstable/httpapi/HttpApiBuilder |
@effect/opentelemetry/OtlpTracer |
effect/unstable/observability/OtlpTracer |
@effect/opentelemetry/OtlpLogger |
effect/unstable/observability/OtlpLogger |
@effect/sql/SqlClient |
effect/unstable/sql/SqlClient |
@effect/sql/SqlResolver |
effect/unstable/sql/SqlResolver |
effect/FiberRef |
effect/References (as Context.Reference) |
effect/TRef |
effect/TxRef |
effect/TestClock |
effect/testing/TestClock |
effect/JSONSchema |
effect/JsonSchema (barrel) |
@effect/platform-node/NodeContext |
@effect/platform-node (re-export) + NodeRuntime.runMain |
effect/Runtime |
deleted — Context is runtime |
Porting a file — annotated diff
Section titled “Porting a file — annotated diff”A real file before and after. Annotations call out each load-bearing change. Comments are // v3: / // v4: inline; no other semantics change.
// ============ v3 — src/mailer.ts ============import { Context, Effect, Layer, FiberRef, STM, TRef } from "effect"import { FileSystem } from "@effect/platform"import * as Either from "effect/Either"import { SqlClient } from "@effect/sql"
export class LogLevel extends FiberRef.Tag("app/LogLevel")<LogLevel, string>() { static readonly initial = "Info"}
export const currentLogLevel = FiberRef.make("Info")
export class Mailer extends Context.Tag("app/Mailer")<Mailer, { send(to: string): Effect.Effect<void, Error>}>() {}
export const MailerLive = Layer.scoped( Mailer, Effect.gen(function* () { const fs = yield* FileSystem.FileSystem const logLevel = yield* FiberRef.get(currentLogLevel) // v3: get returns value directly, yieldable const counter = yield* TRef.make(0) // v3: yieldable TRef const body: Either.Either<Error, string> = Either.right("hello") yield* Effect.locally(Effect.log("sending"), currentLogLevel, "Debug") yield* STM.commit(STM.update(counter, (n) => n + 1)) return Mailer.of({ send: (to) => Effect.gen(function* () { const c = yield* STM.commit(STM.get(counter)) // v3 STM yield* Effect.catchAll(() => Effect.void)(Effect.fail(new Error("oops"))) yield* Effect.fork(Effect.void) // v3 fork = forkChild yield* Effect.forkDaemon(Effect.void) }) }) }))// ============ v4 — src/mailer.ts ============import { Context, Effect, Layer, TxRef } from "effect" // v4: FiberRef → References, TRef → TxRefimport { FileSystem } from "effect" // v4: platform consolidated into effectimport * as Result from "effect/Result" // v4: Either → Resultimport { SqlClient } from "effect/unstable/sql" // v4: sql is unstable pathimport { References } from "effect" // v4: MinimumLogLevel etc. live here
// v4: LogLevel is just a Context.Reference with defaultValue — FiberRef.Tag deletedexport class LogLevel extends Context.Reference<LogLevel>()("app/LogLevel", { defaultValue: () => "Info" as const})
// v4: Context.Service — not Tag; identifier is second callexport class Mailer extends Context.Service<Mailer, { send(to: string): Effect.Effect<void, Error>}>()("app/Mailer") {}
// v4: Layer.scoped → Layer.effect (Scope is automatic, Exclude<R,Scope>)export const MailerLive = Layer.effect( Mailer, Effect.gen(function* () { const fs = yield* FileSystem.FileSystem // v4: FiberRef.get removed — read the Reference via yield* ref, or Effect.service const level: string = yield* LogLevel // v4: TxRef is a plain value — not yieldable; call accessors explicitly const counter = yield* TxRef.make(0) // v4: Result<E,A> is error-first; succeed is Result.succeed, Right → Success const body: Result.Result<string, Error> = Result.succeed("hello") // v4: FiberRef locally → Effect.provideService for References — lexically scoped yield* Effect.provideService(Effect.log("sending"), LogLevel, "Debug") // v4: STM → TxRef atomically (or Effect.transaction for multi-ref tx) yield* TxRef.update(counter, (n) => n + 1) return Mailer.of({ send: (to) => Effect.gen(function* () { const c = yield* TxRef.get(counter) // v4: explicit get // v4: catchAll → catch yield* Effect.catch(() => Effect.void)(Effect.fail(new Error("oops"))) // v4: fork → forkChild ; forkDaemon → forkDetach yield* Effect.forkChild(Effect.void) yield* Effect.forkDetach(Effect.void) }) }) }))
// Additional mechanical port that would appear elsewhere in the file set:// Scope.extend → Scope.provide// Layer.setScheduler(s) → Layer.succeed(Scheduler.Scheduler, s)// @effect/platform/HttpClient → effect/unstable/http/HttpClient// OtlpTracer: @effect/opentelemetry/OtlpTracer → effect/unstable/observability/OtlpTracer// Data.struct({a:1}) → {a:1} (plain objects now structurally equal)// Array.liftEither(f) → Array.liftResult(f)// Effect.either → Effect.result// Effect.optionFromOptional → Effect.catchTag("NoSuchElement", () => Option.none)What changed in the diff, summarized:
effect/FiberRef → effect/References + Context.Reference— declaration, defaultValue, andprovideServicemutation.effect/Either → effect/Resultwith param flipEither<L,R> → Result<E,A>; constructorsright/left → succeed/fail, extractorsgetLefts → getFailures.@effect/platform/* → effect/FileSystem+effect/unstable/*— two destinations: core (FileSystem,Path) intoeffectbarrel; protocol modules (http,sql,observability,ai) intoeffect/unstable/*.TRef → TxRef+ all STM combinators; broaderSTM → Tx*rename.Layer.scoped → Layer.effect— dropLayer.scopedentirely;Scopeis auto-excluded.Scope.extend → Scope.provide.Effect.catchAll → catch,catchAllCause → catchCause,fork → forkChild,forkDaemon → forkDetach.- Structural equality — remove
Data.struct/array/tuplewrappers; plain literals are structurally compared (withNaN === NaNsemantics; useEquivalence.strictEqualonly if you needNaN !== NaN).
Bundle size — why Micro is gone
Section titled “Bundle size — why Micro is gone”From MIGRATION.md: a minimal Effect program tree-shakes to ~6.3 KB min+gz (~15 KB with Schema). Because the rewritten core runtime is the lightweight runtime, the separate Micro namespace was deleted entirely (Micro → Effect, Micro.TaggedError → Data.TaggedError). If you used Micro.forkScoped/forkIn/schedule* for small lambdas, the same combinators now live on Effect with the same semantics.
Schedule redesign — min/max/while
Section titled “Schedule redesign — min/max/while”The old interval-set abstraction (ScheduleInterval, ScheduleIntervals) was removed. Schedules now express only a relative Duration per step via Schedule.fromStep, and standard policy composition is:
- Intersect (recur while both recur, wait for slower):
Schedule.max(a, b)(v3intersect) - Union (recur while either recurs, wait for faster):
Schedule.min(a, b)(v3union) - Clamp:
Schedule.modifyDelay(schedule, ({ delay }) => Duration.min(delay, "10 seconds"))
ScheduleDecision: deleted. Schedule.Decision: likewise — use Schedule.fromStep.
Keep-alive, runtime, and memoization — the invisible shifts
Section titled “Keep-alive, runtime, and memoization — the invisible shifts”Three changes that never appear in your editor’s red squiggles but change production behavior.
Keep-alive is now automatic
Section titled “Keep-alive is now automatic”In v3 a program that forked background work and then completed its main fiber would exit underneath that work unless the main called runMain plus manual reference holding. v4 installs a reference-counted keep-alive timer in core (chapter 13 note, migration/fiber-keep-alive.md): as long as any fiber lives — including forkChild background tasks — the process stays open.
// v3 — exits before background log appears unless runMain held the process:await Effect.runPromise(Effect.gen(function* () { yield* Effect.forkChild(Effect.forever(Effect.log("background")))}))
// v4 — same code keeps the process open until you interrupt it.// Layer.launch + NodeRuntime.runMain still gives you SIGINT/SIGTERM handling,// but you no longer need runMain solely to stay alive.Rule: runPromise now keeps alive (via core timer); runMain adds signals and exit codes. Embeddings via ManagedRuntime keep alive while the runtime is not dispose()d.
Runtime<R> is deleted — Context<R> is the runtime
Section titled “Runtime<R> is deleted — Context<R> is the runtime”v3:
import { Runtime } from "effect"const rt: Runtime<MyServices> = yield* Effect.runtime()await Runtime.runPromise(rt)(Effect.service(MyServices).pipe(Effect.flatMap((s) => s.doThing())))v4:
import { Context, Effect } from "effect"const ctx: Context.Context<MyServices> = yield* Effect.context<MyServices>()await Effect.runPromise(Effect.service(MyServices).pipe(Effect.flatMap((s) => s.doThing())).pipe( Effect.provide(ctx) // or Effect.runPromiseWith(ctx)(effect)))// Also: Effect.runForkWith(ctx), runSyncWith, runCallbackWithRuntimeFlags (bitfield controlling time, tracing toggles) are gone — those behaviors live as References (TracerEnabled, TracerTimingEnabled, MinimumLogLevel, Scheduler.MaxOpsBeforeYield, Scheduler.Scheduler). Tune them with Effect.provideService.
Layer memoization across provides (v4 is shared)
Section titled “Layer memoization across provides (v4 is shared)”Chapter 10’s demo compressed to the migration lesson:
// v3 — builds twice, logs twice, TWO pools (the "why do I have five pools?" bug):const program = Effect.gen(function* () { const a = yield* Effect.provide(useA, MyServiceLayer) const b = yield* Effect.provide(useB, MyServiceLayer)})await Effect.runPromise(program) // builds === 2
// v4 — builds once (ambient MemoMap), logs once, ONE pool:await Effect.runPromise(program) // builds === 1
// Opt-out when you need freshness (per-test isolation):yield* Effect.provide(useA, Layer.fresh(MyServiceLayer)) // always newyield* Effect.provide(useA, MyServiceLayer, { local: true }) // share within this provide onlyIf your v3 code worked around duplication by threading a single MemoMap manually, delete that workaround — it is now default behavior. Only pass MemoMap explicitly for cross-ManagedRuntime sharing (chapter 29).
Equality — structural by default
Section titled “Equality — structural by default”v3 compared plain objects and arrays by reference. v4 compares them structurally, including nested Maps/Sets, using structural hashing. NaN === NaN for this comparison (use Equivalence.strictEqual if you need NaN !== NaN).
What to delete:
| v3 | v4 |
|---|---|
Data.struct({ a: 1 }) |
{ a: 1 } |
Data.unsafeStruct({ a: 1 }) |
{ a: 1 } |
Data.array([1,2]) |
[1,2] or [...xs] |
Data.tuple(1, "a") |
[1, "a"] |
Data.case({ _tag: "A", x: 1 }) |
{ _tag: "A" as const, x: 1 } |
new Data.Class({ a: 1 }) |
still works — class syntax stays |
Data.TaggedEnum |
stays — discriminated union helpers remain |
Equal.equals(a, b) on plain objects |
now structural — usually just a === b is insufficient; prefer Equal.equals for deep equality |
Hash.hash(obj) |
structural hash |
Keep Data.Class when you want a named constructor with a codec; drop Data.struct/array/tuple when you held plain data. See migration/equality.md.
Micro is gone — core is already micro
Section titled “Micro is gone — core is already micro”Micro (effect/Micro) was v3’s lightweight runtime (≈ 20 combinators). v4’s core rewrite hit the same memory/speed targets, so Micro was deleted with no replacement. Map:
v3 Micro |
v4 |
|---|---|
Micro.succeed / fail / sync / try / promise |
Effect.succeed / fail / sync / try / promise |
Micro.gen / Micro.fn("name") |
Effect.gen / Effect.fn("name") |
Micro.TaggedError |
Data.TaggedError (from effect/Data) |
Micro.forkDaemon |
Effect.forkDetach |
Micro.scheduleExponential(n) |
Schedule.exponential(Duration) |
Micro.CurrentScheduler |
References.Scheduler / Scheduler.Scheduler |
Micro.MaxOpsBeforeYield |
References.MaxOpsBeforeYield |
If you imported Micro for bundle size, remove the import and use Effect directly — the bundler now tree-shakes the same 6 KB.
Step-by-step upgrade recipe
Section titled “Step-by-step upgrade recipe”Adapted from MIGRATION.md guidance, ordered for minimal churn:
- Bump versions together.
pnpm add effect@4.0.0-rc.112 @effect/platform-node@4.0.0-rc.112 @effect/sql-sqlite-node@4.0.0-rc.112 …— one RC across all@effect/*. Do not mix v3 packages. - Fix
Either → Resultfirst. SearchEither. Rename package import, swapEither<Either.Left → Result.Failure, Right → Success>, fixgetLefts → getFailures,liftEither → liftResult, param flip fromEither<L,R>toResult<E,A>. Type errors here clarifycatchhandlers next. - Fix
catchAll → catchfamily. Global searchcatchAll→catch,catchSome → catchFilter(and introduceFilterimports),either → result,optionFromOptional → catchTag. - Fix
forkfamily.fork\b → forkChild,forkDaemon → forkDetach. Add{ startImmediately?: boolean, uninterruptible?: boolean | "inherit" }options only if you had custom interruption logic. - Fix
TRef → TxRef/STM. SearchTRef,TQueue,TMap. PrefixTx. - Collapse services. Search
Context.Tag,GenericTag,Effect.Tag,Effect.Service. Replace withContext.Service(id)class form. Flip identifier order. DeleteContext.TagUnify. - Fix
FiberRef → Reference. SearchFiberRef,FiberRefs. IntroduceContext.Reference(id, { defaultValue })+Effect.provideServicefor overrides. - Delete
Runtime/Scope.extend/Layer.scopedshims.Runtime → Context,Scope.extend → Scope.provide,Layer.scoped → Layer.effect(dropScopefromRafter). Datashims. DeleteData.struct/array/tuple/case/unsafe*wrappers — keepData.ClassandData.TaggedError.- Imports. Run the import map step:
@effect/platform/FileSystem → effect/FileSystem,@effect/platform/HttpClient → effect/unstable/http/HttpClient,@effect/opentelemetry/* → effect/unstable/observability/*,@effect/sql/* → effect/unstable/sql/*. Many editors’ auto-import can finish this once barrel renames are in place. - Run tests in virtual time again.
it.effectstill resetsTestClockper test. If a test that sleeped in v3 now hangs, it missedTestClock.adjustpairing (forkChild → adjust → join). - Provide observability last. If you had custom
RuntimeFlagsto gate tracing, move that toReferences.TracerEnabled/Effect.withTracerEnabled.
Common compilation errors and what they mean
Section titled “Common compilation errors and what they mean”| Error | Cause | Fix |
|---|---|---|
Type 'Ref<number>' is not assignable to 'Effect<…>' |
Yielding a handle where an Effect is expected | Replace yield* ref with yield* Ref.get(ref) (and Queue.take, Deferred.await, Fiber.join likewise) |
Property 'catchAll' does not exist on type 'typeof Effect' |
v3 name | Use Effect.catch / catchCause / catchFilter |
Property 'scoped' does not exist on type 'typeof Layer' |
v3 name | Use Layer.effect / effectContext / effectDiscard |
Cannot find module '@effect/platform/FileSystem' |
Re-homed | from "effect" (barrel) |
Cannot find module 'effect/Either' |
Removed | from "effect/Result" + param flip |
Property 'either' does not exist |
v3 name | Effect.result |
No overload matches 'Effect.fork' |
Renamed | Effect.forkChild(eff) or Effect.forkDetach(eff) |
Argument of type 'typeof MyService' not providing |
Still using Context.Tag |
Migrate to Context.Service("id") and yield* MyService |
Type 'TRef<number>' is not assignable… |
v3 name | TxRef + TxRef.get/update |
FiberRefs / FiberRef errors |
Deleted | Context.Reference + Effect.provideService |
Runtime<…> errors |
Deleted | Context<…> + Effect.context / provide |
Where each v3 pattern now lives in this course
Section titled “Where each v3 pattern now lives in this course”| You learned in v3 as … | Re-read here as … |
|---|---|
Effect.gen + combinators |
ch 03 — Effect.fn("name") and Effect.fn.Return |
Typed errors + catchAll |
ch 05 — reason errors, Filter, catchReason |
Cause trees |
ch 06 — flat Cause { reasons: Reason[] } |
Option/Either/Data |
ch 07 — Option, Result<E,A>, Data.Class |
Context.Tag / GenericTag |
ch 09 — Context.Service |
Layer + Layer.scoped |
ch 10 — Layer.effect with auto Scope, memoization |
Config + providers |
ch 11 — unchanged shape, tighter path typing |
Scope + acquireRelease |
ch 12 — Scope.provide |
Fiber, fork, race |
ch 13 — forkChild/forkDetach, structured lifetimes |
FiberRef |
ch 14 — Context.Reference |
Schedule intervals |
ch 17 — min/max/while redesign |
Stream methods |
ch 18 — channel-backed, same Stream surface |
Schema as validation |
Schema track / ch 20+ — single source + arbitraries |
HttpApi, HttpClient |
ch 24 — effect/unstable/httpapi, status-annotated errors |
SqlClient, Migrator |
ch 25 — effect/unstable/sql + driver packages |
TestClock, it.effect |
ch 28 — same it.effect/TestClock shape |
Further reading — upstream migration docs
Section titled “Further reading — upstream migration docs”These live alongside the code and are the canonical reference as RCs advance:
| Document | Covers |
|---|---|
MIGRATION.md |
Philosophy, package consolidation, unstable system, versioning, bundle |
migration/v3-to-v4.md |
Exhaustive import + API rename map (generated diff) |
migration/services.md |
Context.Tag → Context.Service detail |
migration/cause.md |
Flat Cause vs recursive tree |
migration/error-handling.md |
catch* renames + Filter + catchReason |
migration/forking.md |
fork → forkChild, forkDaemon → forkDetach, options |
migration/yieldable.md |
Yieldable narrowing contract |
migration/fiber-keep-alive.md |
Keep-alive timer, runMain vs runPromise |
migration/layer-memoization.md |
Cross-provide memo map sharing |
migration/fiberref.md |
FiberRef → Context.Reference |
migration/runtime.md |
Runtime<R> removal |
migration/scope.md |
Scope refinements, extend → provide |
migration/equality.md |
Structural-by-default, Data shims removed |