| name | effect-fiber |
| description | Fork, supervise, and interrupt Effect fibers with Effect.forkChild/forkScoped/forkIn/forkDetach, Fiber join/await/interrupt, uninterruptible regions, and the FiberHandle/FiberMap/FiberSet supervision collections. Use when running background work, cancelling or restarting tasks, implementing latest-wins or keyed workers, bridging Effect into callback APIs, or debugging interruption and fiber lifetime issues. |
You are an Effect TypeScript expert specializing in fiber lifecycle, interruption, and supervision with Fiber, FiberHandle, FiberMap, and FiberSet.
Effect Source Reference
The Effect v4 source is available at ~/.cache/effect-v4/.
Browse and read files there directly to look up APIs, types, and implementations.
Reference this for:
Fiber interface and await/join/interrupt operations (packages/effect/src/Fiber.ts)
- Fork variants, interruption combinators, run* APIs (
packages/effect/src/Effect.ts)
- Single-slot supervision (
packages/effect/src/FiberHandle.ts)
- Keyed fiber collections (
packages/effect/src/FiberMap.ts)
- Grow-only fiber collections (
packages/effect/src/FiberSet.ts)
- Runtime internals —
FiberImpl, fork/interrupt mechanics (packages/effect/src/internal/effect.ts)
- v3 → v4 fork renames (
migration/forking.md), keep-alive changes (migration/fiber-keep-alive.md — partially stale, see section 9)
- Real usage and edge cases (
packages/effect/test/FiberHandle.test.ts, FiberMap.test.ts, FiberSet.test.ts)
Core Model
A Fiber<A, E = never> is a handle to a lightweight, cooperatively scheduled execution of an Effect that may still be running or may have completed. It is the unit of concurrency in Effect. A fiber's outcome is an Exit<A, E> — success with A, or failure with a Cause<E> that can contain typed errors, defects, and interruptions.
import {
Cause,
Deferred,
Effect,
Exit,
Fiber,
FiberHandle,
FiberMap,
FiberSet,
Schedule,
Scope,
Semaphore
} from 'effect';
Key facts to internalize:
- Structured concurrency. A fiber forked with
Effect.forkChild is attached to its parent: when the parent fiber completes (success, failure, or interruption), all still-running children are interrupted before the parent's exit settles. forkScoped/forkIn tie the fiber's lifetime to a Scope instead; forkDetach produces a global fiber with no automatic lifetime.
- Interruption is cooperative. It is observed at effect boundaries; uninterruptible regions and finalizers run to completion first. Interrupting is itself an effect that waits for the target to fully settle.
- Fiber ids are plain
numbers in v4 (there is no composite FiberId type). Interruptors are recorded in the Cause as a ReadonlySet<number>.
Exit is an Effect. You can yield* an Exit directly to propagate its result into the current fiber.
- Useful synchronous members on the fiber object:
fiber.id, fiber.pollUnsafe(): Exit<A, E> | undefined, fiber.addObserver(cb): () => void (returns an unsubscribe function), fiber.interruptUnsafe(fiberId?).
High-level concurrency combinators (Effect.all, Effect.forEach, Effect.race*) are covered by the effect-parallelization skill — prefer those when you just need concurrent results; reach for explicit fibers when you need lifecycle control.
1. Forking Fibers
The v3 names are gone: Effect.fork → Effect.forkChild, Effect.forkDaemon → Effect.forkDetach (Effect.forkAll and Effect.forkWithErrorHandler were removed). Four variants exist in v4, differing only in lifetime:
| Variant | Interrupted when… | Requirements |
|---|
Effect.forkChild | the parent fiber completes | R |
Effect.forkScoped | the current Scope closes | R | Scope |
Effect.forkIn(effect, scope) | the supplied Scope closes | R |
Effect.forkDetach | never automatically (explicit interrupt only) | R |
All four return Effect<Fiber<A, E>, never, R> — forking never fails — and all accept the same options object:
{
readonly startImmediately?: boolean | undefined;
readonly uninterruptible?: boolean | 'inherit' | undefined;
}
startImmediately: true evaluates the fiber synchronously up to its first suspension. The default is a deferred start: the fiber is scheduled and begins on the next scheduler tick, after the parent yields.
uninterruptible: true starts the fiber uninterruptible; 'inherit' copies the parent's current interruptibility; default is interruptible.
const task = Effect.gen(function* () {
yield* Effect.sleep('2 seconds');
return 'result';
});
const program = Effect.gen(function* () {
const fiber1 = yield* Effect.forkChild(task);
const fiber2 = yield* task.pipe(Effect.forkChild);
const fiber3 = yield* Effect.forkChild(task, { startImmediately: true });
const fiber4 = yield* task.pipe(Effect.forkChild({ startImmediately: true }));
const result = yield* Fiber.join(fiber1);
return result;
});
Forking into a scope:
const scopedFork = Effect.scoped(
Effect.gen(function* () {
const fiber = yield* Effect.forkScoped(task);
const scope = yield* Effect.scope;
const fiber2 = yield* Effect.forkIn(task, scope);
yield* Effect.sleep('1 second');
})
);
Detached fibers survive the parent — pair them with explicit interruption or Fiber.runIn:
const daemon = Effect.gen(function* () {
const fiber = yield* Effect.forkDetach(pollForever);
const scope = yield* Effect.scope;
Fiber.runIn(fiber, scope);
});
Lazy start gotcha
Because forked fibers start on the next tick by default, side effects have not happened immediately after the fork:
const program = Effect.gen(function* () {
let started = false;
const fiber = yield* Effect.forkChild(Effect.sync(() => (started = true)));
yield* Effect.yieldNow;
});
Use { startImmediately: true } when registration must happen before the parent proceeds (e.g. installing an Effect.onInterrupt handler or subscribing to a queue).
2. Joining, Awaiting, and Reading Exits
const program = Effect.gen(function* () {
const fiber = yield* Effect.forkChild(compute);
const value = yield* Fiber.join(fiber);
const exit = yield* Fiber.await(fiber);
if (Exit.isSuccess(exit)) {
console.log(exit.value);
} else if (Cause.hasInterrupts(exit.cause)) {
console.log('interrupted by', Cause.interruptors(exit.cause));
} else {
console.log('failed:', Cause.squash(exit.cause));
}
});
Many fibers at once:
const exits = yield* Fiber.awaitAll([fiberA, fiberB]);
const values = yield* Fiber.joinAll([fiberA, fiberB]);
Working with Exit values:
const summary = Exit.match(exit, {
onSuccess: (value) => `ok: ${value}`,
onFailure: (cause) => `failed: ${Cause.squash(cause)}`
});
Exit.getSuccess(exit);
Exit.getCause(exit);
Exit.hasFails(exit);
Exit.hasDies(exit);
Exit.hasInterrupts(exit);
Cause.hasInterruptsOnly(cause);
const value = yield* exit;
To capture the exit of an inline effect (no fiber needed) use Effect.exit(effect), which returns Effect<Exit<A, E>, never, R>.
Synchronous inspection (outside or inside effects):
const exit = fiber.pollUnsafe();
const cancel = fiber.addObserver((exit) => console.log('done', exit));
cancel();
There is no Fiber.poll effect in v4 — pollUnsafe and addObserver are the low-level hooks. In tests, fiber.pollUnsafe() combined with TestClock from effect/testing is the standard way to assert "still running" vs "completed" (see the effect-concurrency-testing skill for the full idiom; effect-testing covers TestClock setup).
3. Interruption
yield* Fiber.interrupt(fiber);
yield* Fiber.interruptAll([fiber1, fiber2]);
yield* Fiber.interruptAs(fiber, controllerFiber.id);
yield* Fiber.interruptAllAs([fiber1, fiber2], controllerFiber.id);
yield* Effect.interrupt;
Notes:
Fiber.interrupt uses the current fiber's id as the interruptor. The id set ends up in the Cause and is what Effect.onInterrupt finalizers and Cause.interruptors see.
- Fire-and-forget cancellation from synchronous code:
fiber.interruptUnsafe() — sends the signal without waiting.
- Self-interruption via
Effect.interrupt produces an exit where Cause.hasInterruptsOnly is true; platform runMain runners map that to exit code 130 rather than an error.
- A constructed interrupted exit is available as
Exit.interrupt(fiberId?).
Current-fiber accessors:
const self = yield* Effect.fiber;
const id = yield* Effect.fiberId;
const eff = Effect.withFiber((fiber) => Effect.succeed(fiber.id));
const maybe = Fiber.getCurrent();
4. Uninterruptible Regions and Cleanup
Effect.uninterruptible(critical);
Effect.interruptible(effect);
Interruption inside an uninterruptible region is deferred, not dropped: the pending interruption fires the moment the region ends. The canonical acquire/use/release shape protects acquisition and cleanup while keeping the work cancellable:
const withConnection = Effect.uninterruptibleMask((restore) =>
Effect.gen(function* () {
const conn = yield* acquireConnection;
return yield* restore(useConnection(conn)).pipe(
Effect.onExit(() => closeConnection(conn))
);
})
);
Effect.interruptibleMask((restore) => ...) is the inverse: the body is interruptible and restore re-applies the outer interruptibility.
Interruption-specific and general finalizers:
const guarded = Effect.onInterrupt(longRunning, (interruptors) =>
Effect.log(`interrupted by: ${[...interruptors].join(', ')}`)
);
const cleaned = Effect.ensuring(task, Effect.log('cleanup'));
const observed = Effect.onExit(task, (exit) =>
Effect.log(Exit.isSuccess(exit) ? 'ok' : 'failed/interrupted')
);
const onFail = Effect.onError(task, (cause) => Effect.log(Cause.squash(cause)));
Bridging interruption to AbortSignal-aware APIs:
const download = Effect.scoped(
Effect.gen(function* () {
const signal = yield* Effect.abortSignal;
})
);
5. Structured Concurrency Lifetimes
The most important v4 behavior: when a fiber finishes its work, any children forked with forkChild that are still running are interrupted before the parent's exit is reported. This makes fire-and-forget with forkChild a bug:
const wrong = Effect.gen(function* () {
yield* Effect.forkChild(sendAuditLog(event));
return 'done';
});
Correct options, by intent:
const scoped = Effect.gen(function* () {
yield* Effect.forkScoped(sendAuditLog(event));
return 'done';
});
const awaited = Effect.awaitAllChildren(
Effect.gen(function* () {
yield* Effect.forkChild(sendAuditLog(event));
return 'done';
})
);
const detached = Effect.gen(function* () {
const fiber = yield* Effect.forkDetach(sendAuditLog(event));
return 'done';
});
Whichever lifetime you pick, a forked fiber's failure is observed by nobody unless you arrange it: join/await the fiber, supervise it via FiberHandle/FiberMap/FiberSet join, or attach Effect.catchCause/Effect.onError plus logging inside the forked effect. v4 removed Effect.forkWithErrorHandler, and the runtime does not log unhandled fiber failures.
Effect.awaitAllChildren only waits for children forked while the wrapped effect runs — children that existed beforehand are not awaited.
forkIn/forkScoped register an interruption finalizer on the scope and remove it when the fiber completes on its own. Forking into an already-closed scope interrupts the new fiber immediately. The same applies to Fiber.runIn(fiber, scope), which only registers the finalizer — it does not wait for the fiber.
Scope mechanics themselves — Effect.scoped, manual Scope.make/close, finalizer ordering — are covered by the effect-scope skill.
6. FiberHandle — Single-Slot Supervision
FiberHandle<A, E> manages at most one fiber. Running a new effect into it interrupts the previous fiber (latest wins); closing the owning scope interrupts the current fiber; completed fibers remove themselves.
const program = Effect.gen(function* () {
const handle = yield* FiberHandle.make<string, MyError>();
const fiber = yield* FiberHandle.run(handle, task);
yield* FiberHandle.run(handle, otherTask, { onlyIfMissing: true });
const current = FiberHandle.getUnsafe(handle);
const current2 = yield* FiberHandle.get(handle);
yield* FiberHandle.clear(handle);
yield* FiberHandle.awaitEmpty(handle);
yield* FiberHandle.join(handle);
}).pipe(Effect.scoped);
run options: { onlyIfMissing?: boolean; propagateInterruption?: boolean }. The type also accepts startImmediately, but it is a no-op — collection fibers are forked via Effect.runForkWith and always start synchronously (see "How collection fibers relate to the caller" in section 8).
join vs awaitEmpty
FiberHandle.join(handle): Effect<void, E> — fails with the first managed-fiber failure; succeeds (void) only when the handle's scope closes. It never resolves just because a fiber finished successfully. Use it as a supervision watchdog.
FiberHandle.awaitEmpty(handle): Effect<void, E> — waits for the currently held fiber to complete.
propagateInterruption
By default (false), interruption of a managed fiber — including by an external Fiber.interrupt — does not fail join. With propagateInterruption: true, external interruptions do fail join; interruptions performed internally by the collection itself (replacement, clear, scope close — recorded with internal fiber id -1) never do.
Installing existing fibers
const fiber = Effect.runFork(task);
yield* FiberHandle.set(handle, fiber, { onlyIfMissing: true });
FiberHandle.setUnsafe(handle, fiber);
Runtime helpers — synchronous runners for callback code
runtime captures the current services and returns a synchronous function that forks effects into the handle:
const handle = yield* FiberHandle.make<void, never>();
const run = yield* FiberHandle.runtime(handle)<MyService>();
const fiber = run(taskNeedingMyService, { onlyIfMissing: false });
const runPromise = yield* FiberHandle.runtimePromise(handle)<MyService>();
Runner options: { signal?: AbortSignal; scheduler?: Scheduler; onlyIfMissing?: boolean; propagateInterruption?: boolean }.
Shortcuts that create the handle and runner together (scoped):
const run = yield* FiberHandle.makeRuntime<never>();
const runPromise = yield* FiberHandle.makeRuntimePromise();
const promise = runPromise(Effect.succeed('hello'));
Closed-handle behavior
After the scope closes: FiberHandle.run interrupts the calling fiber; the captured runtime/makeRuntime runners instead return a shared, already-interrupted fiber. Fibers passed to setUnsafe on a closed handle are interrupted immediately.
7. FiberMap — Keyed Fibers
FiberMap<K, A, E> is a map of running fibers indexed by key. Running a new effect under an existing key interrupts the previous fiber for that key; entries remove themselves on completion; scope close interrupts everything.
const program = Effect.gen(function* () {
const map = yield* FiberMap.make<string, void, JobError>();
const fiber = yield* FiberMap.run(map, 'job-1', runJob(1));
yield* FiberMap.run(map, 'job-1', runJob(1), { onlyIfMissing: true });
yield* FiberMap.has(map, 'job-1');
yield* FiberMap.get(map, 'job-1');
yield* FiberMap.size(map);
yield* FiberMap.remove(map, 'job-1');
yield* FiberMap.clear(map);
for (const [key, fiber] of map) {
console.log(key, fiber.id);
}
yield* FiberMap.awaitEmpty(map);
yield* FiberMap.join(map);
}).pipe(Effect.scoped);
run options are the same as FiberHandle's: { onlyIfMissing?, propagateInterruption? } (startImmediately is in the type but is a no-op here too). set/setUnsafe install existing fibers under a key with { onlyIfMissing?, propagateInterruption? }.
Runtime helpers take the key as the first runner argument:
const run = yield* FiberMap.runtime(map)<MyService>();
run('job-2', taskNeedingMyService, { onlyIfMissing: true });
const runPromise = yield* FiberMap.runtimePromise(map)<MyService>();
const run2 = yield* FiberMap.makeRuntime<never, string>();
const runPromise2 = yield* FiberMap.makeRuntimePromise<never, string>();
Closed-map behavior matches FiberHandle: FiberMap.run interrupts the caller; runtime runners return a pre-interrupted fiber.
8. FiberSet — Grow-Only Fiber Collections
FiberSet<A, E> tracks an unkeyed set of fibers. No replacement semantics — every run/add grows the set; completed fibers remove themselves; scope close interrupts all. Nothing limits admission: unbounded run calls on a production ingest path are a hazard — gate them with a Semaphore (see the bounded pattern under Key Patterns).
const program = Effect.gen(function* () {
const set = yield* FiberSet.make<void, TaskError>();
const fiber = yield* FiberSet.run(set, handleRequest(req));
yield* FiberSet.add(set, existingFiber);
FiberSet.addUnsafe(set, anotherFiber);
yield* FiberSet.size(set);
yield* FiberSet.clear(set);
const exits = yield* Fiber.awaitAll(set);
yield* FiberSet.awaitEmpty(set);
yield* FiberSet.join(set);
}).pipe(Effect.scoped);
run options: { propagateInterruption? } (no onlyIfMissing — there is no key; startImmediately is in the type but is a no-op). On a closed set, FiberSet.run returns an already-interrupted fiber (it does not interrupt the caller, unlike FiberHandle/FiberMap).
Runtime helpers mirror the others in shape, with one source quirk: the FiberSet.runtime runner's option type accepts propagateInterruption but never forwards it to the set (addUnsafe is called without options) — set it via FiberSet.run/add instead:
const run = yield* FiberSet.runtime(set)<MyService>();
run(taskNeedingMyService);
const runPromise = yield* FiberSet.runtimePromise(set)<MyService>();
const run2 = yield* FiberSet.makeRuntime();
const runPromise2 = yield* FiberSet.makeRuntimePromise();
How collection fibers relate to the caller
Fibers forked via FiberHandle/FiberMap/FiberSet.run (and the runtime runners) are created with Effect.runForkWith(parent.context) — they are root fibers carrying the caller's services, not children of the calling fiber. Consequences:
- They start executing immediately and synchronously up to their first suspension (no lazy start, unlike
Effect.forkChild).
- The calling fiber's completion does not interrupt them — only key replacement,
remove/clear, or the collection's scope close does.
9. Running Fibers at the Program Edge
Effect.runFork starts a root fiber synchronously (it evaluates until the first suspension before returning):
const fiber = Effect.runFork(program, {
signal: abortController.signal,
scheduler: customScheduler,
uninterruptible: true,
onFiberStart: (fiber) => console.log('started', fiber.id)
});
fiber.addObserver((exit) => console.log('done', exit));
Effect.runFork(Fiber.interrupt(fiber));
All keys of Effect.RunOptions are optional: { signal?, scheduler?, uninterruptible?, onFiberStart? }.
When the effect still needs services, pre-apply a Context:
const runWith = Effect.runForkWith(servicesContext);
const fiber = runWith(effectNeedingServices, { signal });
This is exactly what the FiberHandle/Map/Set runtime helpers wrap for you — prefer those when the forked fibers need supervision.
Keep-alive and runMain
In beta.80 there is no per-fiber keep-alive in the core runtime (it existed earlier in v4 but was removed — the migration/fiber-keep-alive.md doc predates the removal). A bare Effect.runFork/Effect.runPromise whose fiber is suspended on a pure Effect primitive (e.g. Deferred.await, Effect.never) does not by itself hold the Node.js process open.
Runtime.makeRunMain-based runners — NodeRuntime.runMain from @effect/platform-node, BunRuntime.runMain, etc. — install a long-interval timer that keeps the process alive until the main fiber completes, and additionally provide SIGINT/SIGTERM handling (interrupting the root fiber gracefully), exit-code mapping (interruption-only causes → 130), and error reporting. Always use runMain for long-lived program entry points:
import { NodeRuntime } from '@effect/platform-node';
NodeRuntime.runMain(program);
See the effect-managed-runtime skill for embedding Effect in existing applications.
Key Patterns
Latest-wins cancellation (FiberHandle + runtime)
Each new request interrupts the in-flight one — autocomplete, file watching, "restart preview on change":
const searchBox = Effect.gen(function* () {
const handle = yield* FiberHandle.make<void, never>();
const run = yield* FiberHandle.runtime(handle)<SearchApi>();
input.addEventListener('input', () => {
run(performSearch(input.value));
});
yield* Effect.never;
}).pipe(Effect.scoped);
Restart-on-crash worker with a supervision watchdog
Effect.retry restarts the worker with backoff; FiberHandle.join propagates a final, unrecovered failure to the supervisor:
const worker = consumeJobs.pipe(
Effect.retry(Schedule.exponential('250 millis').pipe(Schedule.take(10)))
);
const supervisor = Effect.gen(function* () {
const handle = yield* FiberHandle.make<never, JobError>();
yield* FiberHandle.run(handle, worker);
yield* FiberHandle.join(handle);
}).pipe(Effect.scoped);
Keyed jobs with dedupe and per-key cancellation (FiberMap)
const downloads = Effect.gen(function* () {
const map = yield* FiberMap.make<string, void, DownloadError>();
const start = (url: string) =>
FiberMap.run(map, url, download(url), { onlyIfMissing: true });
const cancel = (url: string) => FiberMap.remove(map, url);
yield* start('https://example.com/a.bin');
yield* start('https://example.com/a.bin');
yield* cancel('https://example.com/a.bin');
yield* FiberMap.awaitEmpty(map);
}).pipe(Effect.scoped);
Background task set with graceful drain (FiberSet)
const webhookProcessor = Effect.gen(function* () {
const tasks = yield* FiberSet.make<void, WebhookError>();
for (const event of events) {
yield* FiberSet.run(tasks, handleWebhook(event));
}
yield* FiberSet.awaitEmpty(tasks);
}).pipe(Effect.scoped);
To fail fast when any background task crashes, run the service's main loop against FiberSet.join(tasks) (it fails with the first non-interruption failure).
Bounded background task set (FiberSet + Semaphore)
FiberSet never applies backpressure on its own. Bound admission by taking a semaphore permit before run and releasing it when the task fiber settles:
const boundedProcessor = Effect.gen(function* () {
const tasks = yield* FiberSet.make<void, WebhookError>();
const permits = yield* Semaphore.make(16);
for (const event of events) {
yield* Semaphore.take(permits, 1);
yield* FiberSet.run(
tasks,
handleWebhook(event).pipe(
Effect.ensuring(Semaphore.release(permits, 1))
)
);
}
yield* FiberSet.awaitEmpty(tasks);
}).pipe(Effect.scoped);
See the effect-parallelization skill for Semaphore details (withPermits, withPermitsIfAvailable, PartitionedSemaphore).
Bridging callback APIs into supervised fibers
const socketHandler = Effect.gen(function* () {
const set = yield* FiberSet.make<void, never>();
const run = yield* FiberSet.runtime(set)<MessageStore>();
socket.on('message', (data) => {
run(storeMessage(data));
});
yield* Effect.never;
}).pipe(Effect.scoped);
Protected handoff between fibers (Deferred + interruption-safe cleanup)
const handoff = Effect.gen(function* () {
const deferred = yield* Deferred.make<Result, WorkerError>();
const producer = yield* Effect.forkScoped(
produceResult.pipe(
Effect.onExit((exit) => Deferred.done(deferred, exit)),
Effect.onInterrupt(() => Effect.log('producer cancelled'))
)
);
return yield* Deferred.await(deferred);
});
Common Mistakes
Effect.fork / Effect.forkDaemon don't exist in v4 — they are Effect.forkChild and Effect.forkDetach. Effect.forkAll and Effect.forkWithErrorHandler were removed entirely.
- Fire-and-forget with
forkChild silently dies — children are interrupted when the parent fiber completes. Use forkScoped/forkIn for scope-tied work, forkDetach for detached work, or Effect.awaitAllChildren to wait.
- Asserting right after a fork — forked fibers start on the next scheduler tick by default.
yield* Effect.yieldNow first, or fork with { startImmediately: true }.
- Expecting
Fiber.await to fail — it always succeeds with an Exit. Use Fiber.join to propagate the fiber's failure; use Fiber.await to inspect.
- Looking for
Fiber.poll — there is no effectful poll in v4. Use the synchronous fiber.pollUnsafe() (Exit | undefined) or fiber.addObserver.
- Assuming
Fiber.joinAll cancels the rest on failure — it fails fast but leaves the other fibers running. Follow up with Fiber.interruptAll if they should stop.
- Treating
Fiber.interrupt as instant — it waits for the target to fully settle, including finalizers and uninterruptible regions. For a non-blocking signal use fiber.interruptUnsafe().
- Using
join to wait for completion on FiberHandle/FiberMap/FiberSet — join only resolves on a fiber failure or when the scope closes; successful fibers just leave the collection. Use awaitEmpty to wait for work to finish.
- Expecting external interruption to fail
join — by default it doesn't. Pass { propagateInterruption: true } to run/set/add; the collection's own internal interruptions (replacement, clear, scope close) never fail join either way.
- Expecting
onlyIfMissing: true to error when occupied — it succeeds, returning a shared already-interrupted fiber while keeping the existing one. Check Exit.hasInterrupts(yield* Fiber.await(fiber)) to detect the rejected start.
- Calling
run on a closed collection — FiberHandle.run/FiberMap.run interrupt the calling fiber; FiberSet.run and all runtime() runners return a pre-interrupted fiber instead. Neither throws.
- Assuming collection fibers are children of the caller — they are root fibers created via
Effect.runForkWith with the caller's context: they start immediately and survive the calling fiber; only the collection (scope close, replacement, remove/clear) interrupts them.
- Relying on
Effect.runFork/runPromise to keep Node alive — beta.80 removed the core fiber keep-alive; a fiber suspended on Deferred.await/Effect.never won't hold the process open. Use NodeRuntime.runMain (built on Runtime.makeRunMain).
- Letting interruption leak through acquire/release — wrap the whole sequence in
Effect.uninterruptibleMask and restore only the use phase; pending interruption is delivered as soon as the region ends, so cleanup still runs exactly once.
- Leaving collection type parameters off —
FiberHandle.make() defaults to <unknown, unknown>, making join surface unknown errors. Always pass them: FiberHandle.make<A, E>(), FiberMap.make<K, A, E>(), FiberSet.make<A, E>().
- Using v3
FiberId types — v4 fiber ids are plain numbers; Cause.interruptors(cause) and Effect.onInterrupt finalizers give you ReadonlySet<number>.
- Assuming someone logs a background fiber's failure — the v4 runtime has no unhandled-fiber-failure reporting and
Effect.forkWithErrorHandler is gone. Unobserved failures vanish: join/await the fiber, watch the collection with FiberHandle/FiberMap/FiberSet.join, or build Effect.catchCause/Effect.onError + logging into the forked effect.