Warning: Effeck is v0. The language will change in major, breaking ways. It is not suitable for production use.

24. What Effect provides and what the kernel adds

The kernel is a TypeScript library over effect@4.0.5. This is kernel test T3, written directly against it: a handler that returns without resuming, and the cleanup that follows.

TypeScript
const Ask = operation<void, number>("Ask")

const body = Effect.scoped(Effect.gen(function* () {
  yield* Effect.acquireRelease(note("acquire"), () => note("release"))
  yield* Effect.addFinalizer((exit) => note(`finalizer sees interrupt=${Exit.hasInterrupts(exit)}`))
  const n = yield* Ask(undefined)                  // captures up to the handle below
  yield* note(`never reached ${n}`)
  return n
}))

const result = yield* handle("h", [Ask], body, {
  Ask: () => Effect.as(note("clause returns without resuming"), -100)
})
acquire
clause returns without resuming
finalizer sees interrupt=true
release
handle returned -100

Everything in that test except operation, handle, and the capture is ordinary Effect: Effect.scoped, acquireRelease, addFinalizer, the fiber, and the finalizers that run during abandonment. This chapter lists what Effect provides, what the kernel adds, and the Effect behaviors the design works around.

Provided by Effect 4, unchanged

Machine requirementEffect 4.0.5 mechanismEvidence
A reified, heap-allocated continuationfiber._stack: Array<Primitive>; runLoop is a trampolineinternal/effect.ts:516, 625-679 [Effect]; 100,001 frames captured and resumed (T6a)
One-shot capture at the rootEffect.callback: the first resume wins, later ones are ignoredinternal/effect.ts:1141-1190; F5 [Effect]
Resumptionfiber.evaluate(effect) runs the fiber synchronously on the resumer's JS stackinternal/effect.ts:1155-1164, 583-599; F6 [Effect]
FinalizationOnExit frames run masked; Scopes close finalizers in reverse order, sequentially by defaultinternal/effect.ts:4173-4214, 3982-4003 [Effect]
Cause-aware recoveryEffect.catchCause sees every reason of a causeinternal/effect.ts:2646-2685 [Effect]; the runtime's catchExpected is built on it (K14)
Dynamic bindingfiber.context plus restore frames; services keyed by stringinternal/effect.ts:2186-2211; Context.ts:609 [Effect]
InterruptionMasks, delivery points, recovery handlers skipped while interruptinginternal/effect.ts:558-579, 4557-4653; internal/core.ts:600-631 [Effect]
Interruptible acquisitionacquireRelease(acquire, release, { interruptible: true }) runs the acquire under restore and still registers the release under the maskEffect.ts:13366-13370; internal/effect.ts:4143-4159; K15a-K15c [Effect]
Structured concurrencyforkChild children are interrupted and awaited before the parent's exit is published; forkIn and forkScoped tie a fiber to a Scopeinternal/effect.ts:600-606, 5491-5673; F8 [Effect]
Fork snapshots ContextA child starts with the parent's Context object; nothing merges backinternal/effect.ts:5536; F7 [Effect]
FairnessshouldYield after MaxOpsBeforeYield = 2048 ops; per-fiber dispatchers batch into one setImmediateScheduler.ts:198-200, 294-297, 231-257 [Effect]
Layer sharing and ownershipMemoMap keyed by Layer object identity; each Effect.provide owns one ScopeLayer.ts:418-466; internal/layer.ts:8-22 [Effect]
AsyncLocalStorage across root waitsFiber re-enters through a captured AsyncResource (effect#8629)internal/effect.ts:586-598 [Effect]

Missing, supplied by the kernel

RequirementWhy Effect cannot express it todayKernel mechanismEvidence
A delimiter inside a fiberThe only delimiter is the root; nothing captures part of _stack (internal/effect.ts:516, 680-701)PromptFrame pushed from Effect.withFiberT1, T9
Partial capture and resumeNo API splices _stackcapture splices the segment above the prompt; resume reinstalls a prompt plus the segment and restores Γ, ιT1, T13, T6a
Tail resume from a compiled clauseA generator clause's frame sits above the owner until the resumed computation returnsClauseFrame performs a tailResume request the clause returns, after the clause's frames are goneK11a-K11d
Abandonment with finalizationA dropped segment skips its OnExit framesabandon re-pushes the segment over a collector and unwinds it with a sticky interruption that joins, never replaces, real pending interruptions, travels with any capture made during the unwinding, and reports every cleanup reasonT3, T4, kernel-edges, A01-A04, I03, I10, I11, K12a-K12e
OwnershipNothing owns a detached segmentOwnerFrame under each clause; transfer to the Scope region around the handler, with an owner token; FiberExitFrame releases transfers when the capturing fiber exitsT3, T10, T14, O01-O08, K13a-K13e
Recovery from expected failures onlyEffect.catch recovers the first Fail of a cause that also holds a defectcatchExpected re-fails any cause with a Die or Interrupt reasonK14a-K14c
Double-resume detectionEffect.callback silently ignores later resumesContinuationAlreadyConsumed defect at the callerT2
Reporting a lost race to the producerThe late resume returns nothingsuspend's resumer returns false; handoff releases late values after producer quiescenceT5a, T5b, H1-H4
Bounded wake-up depthNo depth guard on synchronous wake-upssuspend delivers inline under a budget of 64 nested wake-ups, then schedulesF10a, K10a, K10b

Effect 4.0.5 hazards the language designs around

HazardEvidenceConsequence for Effeck
Effect.gen, Effect.fn, fnUntraced, and fnUntracedEager never call the generator's throw() or return(). JavaScript catch, finally, and using inside the body do not run on failure or interruption.F1-F4 [Effect]; research §8. Effect's language service exempts try/finally in Effect.gen as "valid cleanup" (language-service#442), which the runtime contradicts.Source catch, finally, and using compile to Effect frames, never to JavaScript try, in any function that can pause (Chapter 26).
Effect.catch recovers a cause that holds both a Fail and a Die, and drops the Die.Review probe R1; internal/effect.ts:158-165, 2665-2685 [Effect]Source catch compiles to catchExpected (D13, K14).
Effect.acquireRelease masks its acquire by default, so a wait inside it cannot be cancelled.Review probe R3, K15a [Effect]Resources that wait on the outside world pass { interruptible: true }; using keeps the resource's choice (D25).
Synchronous wake-ups have no depth guard. A chain of Deferred, PubSub, fiber-exit, or Effect.callback wake-ups overflows: one fiber dies with RangeError, and every fiber after it waits forever with no error surfaced.F10a [Effect]. scripts/wakeup-chain-depth.ts breaks on Node 26.10 after about 940 to 1,260 hops, and on Bun 1.4.1 after about 4,100; the research run saw 815 on Node and a Bun livelock at 98% CPU. A regression: effect 3.22.3 completes the same chain at 100,000 hops on both hosts (F10b).The kernel bounds its own wake-ups (K10a). Effect's own Deferred and PubSub need an upstream fix (G5).
A generator that yields completed Exits bypasses the op budget.Research §2: 200,000 yields of Effect.void before another fiber ran.The compiler never emits a yield of a known-completed effect inside a loop.
Hidden forks: timeout, race, raceFirst, concurrent all/forEach, zip with concurrent: true, parallel Scope finalizers, and Layer.merge run their arguments on other fibers.T7b and the fiber-behavior probes [Kernel]; research §4.Control operations cannot cross these combinators (EK4201), and TypeScript functions with a control operation in R are rejected (EK4202).
A service's runtime identity is its key string; two services with the same string alias silently.Context.ts:609; research §5.The compiler generates key strings from package and module path and rejects duplicates program-wide (Chapter 4).
Effect.ensuring requires a cleanup whose E is never; Effect.onExit lets the cleanup fail.Effect.ts (ensuring, onExit signatures); prototype finding 11finally lowers to Effect.onExit (D46), so a failing cleanup is reported, never rejected by the types.
Effect's Context.Service class form makes the class's instance type the service identifier.Prototype finding 19; prototype/esc/scratch/probe2.tsServices compile to an interface plus a Context.Key class, so implements and parameters typed with the service work (D45).
Structurally identical abstract classes are one TypeScript type.experiments/scratch-abstract-services/probe.ts: providing one of two same-shaped classes leaves R empty, and the program dies with Service not foundService classes are nominal (Chapter 4).
Frames are closures over the fiber that pushed them.internal/effect.ts:2200-2208; T14b [Kernel].Every continuation consumption is fiber-affine (Chapter 21).
fnUntracedEager runs the body when the function is called.Research §8.Never emitted: it breaks the rule that constructing a program runs nothing.
Effect.fn costs 1.1 µs (Bun) to 2.0 µs (Node) per call; fnUntraced costs about 0.1 µs.Research §12.Compiled functions use untraced frames; spans are opt-in (tracing, Chapter 28).
Sibling Effect.provide calls do not share Layer builds unless something gives them a common MemoMap: an enclosing provide, a ManagedRuntime, or an explicitly supplied Layer.CurrentMemoMap. The rc.118 migration note says sharing is the default.Research §6; conformance test D05.Each Effect.provide builds its layers once; provide the application layer once at main, or run entry points through a ManagedRuntime (D21, Chapter 4).
Two providers for one service in a Layer.merge: the later one wins, with no warning.A3 probe [Effect].Ambiguity is a compile-time error in wire (EK3202, Chapter 4).
A Layer cycle through Layer.suspend hangs on its own in-progress build.Research §6.wire detects provider cycles statically (EK3203, Chapter 4).
Effect 3 values die in the Effect 4 runtime and vice versa.Research §10.Target Effect 4 only. Effect 3 code is foreign code behind a boundary (Chapter 10).

The upstream draft index collects reproductions and proposed fixes for wake-up depth, generator cleanup and tooling, mixed-cause recovery, fairness, Layer memoization documentation, and interruption diagnostics, plus supported-prompt and segmented-stack proposals. Every entry is drafted, not filed (D59). Kernel review defects are tracked separately in experiments/KERNEL-CHANGES.md.

Scheduling, stack safety, and cancellation

Scheduling [Effect] [Rule]. A fiber runs until it parks, completes, or reaches 2048 run-loop steps, then yields through its dispatcher. Woken fibers run on the waker's JS stack (Deferred, PubSub, fiber exit, Effect.callback) or through a dispatcher task (Queue.offer, Latch.open, Semaphore.release). Effeck makes no promise about whether a woken fiber runs before or after the waker's next step; programs that need an order must synchronize explicitly. Fairness is promised only at run-loop steps. A plain JavaScript region is atomic, exactly as an Effect.sync body is.

Kotlin and Swift never run a woken coroutine on the waker's stack: Kotlin to avoid stack overflow, Swift to avoid taking over the waker's thread for an unbounded time (continuation-prior-art.md rule 12). The kernel keeps Effect's synchronous fast path and bounds its depth instead (K10a). Both choices satisfy the rule above, and Effeck keeps the bounded fast path: always scheduling would add one dispatcher hop to every wake-up, and Effect's own wake-ups already run inline. A measurement that shows waker-blocking time mattering in real programs would change this default, not the rule.

Stack safety [Effect] [Kernel] [Rule]. Heap frames make recursion depth a heap question: T6a nests 100,001 compiled calls and captures all of them in one operation. Direct JavaScript recursion is bounded by the host: about 10,150 frames on Node 26.10 and about 39,900 on Bun 1.4.1 for a one-line recursive function (F9). Plain JavaScript functions recurse on that stack, as any JavaScript does (Chapter 15). Capture and resume copy the frames between the perform site and the prompt; the cost is linear in that distance (Chapter 25). Cross-fiber wake-ups are stack-safe only when bounded (K10a); Effect's own are not (F10a).

Cancellation [Effect] [Kernel]. Interruption is Effect's: a request is recorded, delivered at once to a parked fiber and at the next step to a running one, deferred by masks, and unwinds with recovery handlers skipped. The kernel adds three things. A held continuation is abandoned through its owner as part of the same unwinding (T4). Abandonment re-raises if a masked handler swallows it (kernel-edges). And a pending interruption is reported beside every cleanup failure, never instead of one (K12a).

Division of labor

ConcernEffect 4, unchangedKernel (runtime extension)Compiler
Frames, trampolining, fairnessRun loop, _stack, op budgetEmits frames that satisfy the frame invariant (Chapter 26); never yields known Exits in loops
Root suspensionEffect.callbacksuspend: reports lost races, bounds wake-up depth, releases deferred valuesLowers adapters (Chapter 10)
Partial capturePrompt, Owner, Clause, capture, resume, tailResume, resumeWith, abandon, transferLowers handle, operation calls, and tail resumes; checks second-class continuations (Chapter 9)
FinalizationOnExit, ScopesAbandonment by sticky interruption; fiber-exit release of transfersLowers finally and using to frames (Chapter 26)
Context and DIupdateContext, Context keys, Layers, MemoMapPrompt evidence entriesService keys and accessors, contract checks (EK3101), wire graphs (EK3201-EK3204)
InterruptionMasks, delivery, skip loopOwner abandonment during unwindingNothing beyond masks in source
ForkforkChild, forkIn, Context snapshotPrompts do not cross (HandlerNotOnThisFiber)Block regions for fork; fiber-behavior table; EK4201, EK4202
Typed failuresFail causes, catchCausecatchExpectedthrow → fail; catch → catchExpected; contracts (EK2001, EK2002)
Plain JavaScript callsControl classes; $direct variants (Chapter 15)
ProducersYield as a control operation; transfer; the Stream adapterfunction* and for...of lowering (Chapter 26)