Skip to content

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.

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.

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 up

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.

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 #3

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.