Interruption
Interruption is how Effect cancels work it no longer needs: the loser of a race, every child of a fiber that just failed, or a request that hit its timeout. The crucial guarantee is that interruption is safe — when a fiber is interrupted, all of its finalizers run and every scoped resource is released before the fiber is considered done. You cancel work without leaking it.
import { Effect } from "effect"
const program = Effect.gen(function*() { yield* Effect.log("start") yield* Effect.sleep("2 seconds")
// Explicitly interrupt the current fiber. Nothing after this line runs. yield* Effect.interrupt
yield* Effect.log("never reached")})
Effect.runFork(program)// Logs "start", then the fiber terminates with an Interrupt cause —// "never reached" is never logged.The interruption model
Section titled “The interruption model”A naive cancellation scheme lets one fiber forcibly kill another at any instruction. That risks leaving shared state half-modified. Effect avoids this with asynchronous interruption: a fiber is sent an interruption signal, and the runtime acts on it only at safe points between operations — never in the middle of an atomic step. Crucially, you don’t have to manually poll a flag; the runtime handles the signaling, and you mark the few regions that must not be cut short as uninterruptible.
A fiber that is interrupted finishes with an Exit whose
Cause contains an Interrupt, recording the id of the fiber
that requested the cancellation.
Running cleanup on interruption
Section titled “Running cleanup on interruption”Effect.onInterrupt registers a finalizer that runs only when the effect is
interrupted — not on success, not on ordinary failure. It’s the right hook for
cancellation-specific cleanup, like rolling back a half-applied change or
emitting a “cancelled” metric.
import { Effect, Fiber } from "effect"
const guarded = Effect.gen(function*() { yield* Effect.log("doing work") yield* Effect.sleep("1 second")}).pipe( // The set of interruptor fiber ids is passed in, if you need it. Effect.onInterrupt(() => Effect.log("interrupted: cleaning up")))
const program = Effect.gen(function*() { const fiber = yield* Effect.forkChild(guarded) yield* Effect.sleep("100 millis") yield* Fiber.interrupt(fiber)})
Effect.runFork(program)// doing work// interrupted: cleaning upUninterruptible regions
Section titled “Uninterruptible regions”Sometimes a sequence must run to completion once started — claiming a lock and
recording that you did, committing then acknowledging. Wrap such a critical
section in Effect.uninterruptible so an interruption signal is deferred until
the region finishes.
import { Effect } from "effect"
const transfer = Effect.uninterruptible( Effect.gen(function*() { yield* Effect.log("debit account A") yield* Effect.log("credit account B") // Even if an interrupt arrives mid-transfer, both steps complete first; // the interruption takes effect only after this block returns. }))For finer control, Effect.uninterruptibleMask makes a region uninterruptible
but hands you a restore function to re-open interruptibility for the parts that
should be cancellable — typically the blocking wait in the middle.
import { Effect } from "effect"
const acquireUseRelease = Effect.uninterruptibleMask((restore) => Effect.gen(function*() { yield* Effect.log("acquire lock") // protected — must not be skipped
// `restore` runs the long, blocking part interruptibly so a cancellation // request isn't stuck waiting for the whole region. yield* restore(Effect.sleep("10 seconds"))
yield* Effect.log("release lock") // protected }))Effect.interruptible is the inverse: it re-enables interruption inside an
otherwise uninterruptible region.
Interruption of concurrent effects
Section titled “Interruption of concurrent effects”When you run effects concurrently and one is interrupted (or fails), the others
that are still running are interrupted too. The resulting Cause records each
fiber that was interrupted, combined in parallel.
import { Effect } from "effect"
const program = Effect.forEach( [1, 2, 3], (n) => Effect.gen(function*() { yield* Effect.log(`start #${n}`) yield* Effect.sleep(`${n} seconds`) // The slower siblings get interrupted when this one cancels everything. if (n > 1) { yield* Effect.interrupt } yield* Effect.log(`done #${n}`) }).pipe(Effect.onInterrupt(() => Effect.log(`interrupted #${n}`))), { concurrency: "unbounded" })
Effect.runFork(program)// start #1 / start #2 / start #3// done #1// interrupted #2 / interrupted #3Inspecting interruption
Section titled “Inspecting interruption”Because an interrupted fiber’s outcome is an Exit carrying a Cause, you can
distinguish interruption from real failure when you handle the result. Cause
provides predicates like Cause.hasInterrupts (the cause contains any
interruption) and Cause.hasInterruptsOnly (the cause is interruption only, with
no genuine failures):
import { Cause, Effect, Exit, Fiber } from "effect"
const program = Effect.gen(function*() { const fiber = yield* Effect.forkChild(Effect.never) yield* Effect.sleep("50 millis") yield* Fiber.interrupt(fiber)
// `Fiber.await` returns the Exit without re-raising the interruption. const exit = yield* Fiber.await(fiber) if (Exit.isFailure(exit) && Cause.hasInterruptsOnly(exit.cause)) { yield* Effect.log("fiber was cancelled, not a real error") }})This distinction matters for retries and error reporting: an interruption is a
deliberate cancellation, not a fault, so you usually don’t want to retry it or
alert on it. See Error Management for working with Cause
in depth.