Skip to main content
Effect’s interruption model provides structured cancellation with automatic resource cleanup.

Interruption Basics

Interrupting Fibers

import { Effect, Fiber } from "effect"

const program = Effect.gen(function*() {
  // Start a long-running operation
  const fiber = yield* Effect.fork(
    Effect.gen(function*() {
      yield* Effect.sleep("10 seconds")
      return "completed"
    })
  )
  
  // Interrupt after 1 second
  yield* Effect.sleep("1 second")
  yield* Fiber.interrupt(fiber)
  
  console.log("Fiber interrupted")
})

Automatic Interruption

Effects are automatically interrupted when their scope ends:
import { Effect } from "effect"

const program = Effect.gen(function*() {
  // Start background task
  yield* Effect.fork(
    Effect.forever(
      Effect.gen(function*() {
        yield* Effect.sleep("1 second")
        yield* Effect.log("Still running...")
      })
    )
  )
  
  yield* Effect.sleep("5 seconds")
  
  // When this effect completes, forked fiber is automatically interrupted
})

Interruption Handlers

onInterrupt

Run cleanup when interrupted:
import { Effect, Ref } from "effect"

const program = Effect.gen(function*() {
  const cleanedUp = yield* Ref.make(false)
  
  const fiber = yield* Effect.fork(
    Effect.never.pipe(
      Effect.onInterrupt(() =>
        Effect.gen(function*() {
          yield* Ref.set(cleanedUp, true)
          yield* Effect.log("Cleanup completed")
        })
      )
    )
  )
  
  yield* Fiber.interrupt(fiber)
  
  const wasCleanedUp = yield* Ref.get(cleanedUp)
  console.log(`Cleaned up: ${wasCleanedUp}`) // true
})

ensuring

Guarantee finalization regardless of success, failure, or interruption:
import { Effect, Ref } from "effect"

const program = Effect.gen(function*() {
  const finalized = yield* Ref.make(false)
  
  const task = Effect.gen(function*() {
    yield* Effect.sleep("10 seconds")
    return "done"
  }).pipe(
    Effect.ensuring(Ref.set(finalized, true))
  )
  
  const fiber = yield* Effect.fork(task)
  yield* Fiber.interrupt(fiber)
  
  const wasFinalized = yield* Ref.get(finalized)
  console.log(`Finalized: ${wasFinalized}`) // true
})

Uninterruptible Regions

Making Effects Uninterruptible

import { Effect } from "effect"

const criticalSection = Effect.gen(function*() {
  yield* Effect.log("Starting critical operation")
  yield* Effect.sleep("2 seconds")
  yield* Effect.log("Critical operation complete")
}).pipe(
  Effect.uninterruptible
)

const program = Effect.gen(function*() {
  const fiber = yield* Effect.fork(criticalSection)
  
  // Try to interrupt immediately
  yield* Fiber.interrupt(fiber)
  
  // Interruption will wait until critical section completes
})
Use uninterruptible sparingly. Long uninterruptible regions prevent graceful shutdown and can cause resource leaks.

Interruptible Regions

Make specific parts interruptible within an uninterruptible region:
import { Effect } from "effect"

const program = Effect.gen(function*() {
  // Critical setup (uninterruptible)
  yield* Effect.log("Critical setup")
  
  // Safe point where interruption is allowed
  yield* Effect.interruptible(
    Effect.gen(function*() {
      yield* Effect.sleep("5 seconds")
      yield* Effect.log("Safe operation")
    })
  )
  
  // Critical cleanup (uninterruptible)
  yield* Effect.log("Critical cleanup")
}).pipe(
  Effect.uninterruptible
)

Resource Management

acquireUseRelease

Acquisition is uninterruptible, use is interruptible, release always runs:
import { Effect, Ref } from "effect"

const program = Effect.gen(function*() {
  const released = yield* Ref.make(false)
  
  const fiber = yield* Effect.fork(
    Effect.acquireUseRelease(
      Effect.gen(function*() {
        yield* Effect.log("Acquiring resource")
        return { id: 1, name: "resource" }
      }),
      (resource) =>
        Effect.gen(function*() {
          yield* Effect.log(`Using resource ${resource.id}`)
          yield* Effect.never // Block forever
        }),
      (resource) =>
        Effect.gen(function*() {
          yield* Effect.log(`Releasing resource ${resource.id}`)
          yield* Ref.set(released, true)
        })
    )
  )
  
  yield* Effect.sleep("100 millis")
  yield* Fiber.interrupt(fiber)
  
  const wasReleased = yield* Ref.get(released)
  console.log(`Released: ${wasReleased}`) // true
})

Scope-based Resource Management

import { Effect, Scope } from "effect"

const acquireResource = Effect.gen(function*() {
  const resource = { id: 1 }
  
  yield* Effect.addFinalizer(() =>
    Effect.log(`Cleanup resource ${resource.id}`)
  )
  
  return resource
})

const program = Effect.gen(function*() {
  const fiber = yield* Effect.fork(
    Effect.scoped(
      Effect.gen(function*() {
        const resource = yield* acquireResource
        yield* Effect.log(`Using ${resource.id}`)
        yield* Effect.never
      })
    )
  )
  
  yield* Effect.sleep("100 millis")
  yield* Fiber.interrupt(fiber)
  // Finalizer runs automatically
})

Interruption Checking

isInterrupted

import { Effect } from "effect"

const program = Effect.gen(function*() {
  const interrupted = yield* Effect.isInterrupted
  
  if (interrupted) {
    yield* Effect.log("Effect was interrupted")
  }
})

allowInterrupt

Create explicit interruption checkpoints:
import { Effect } from "effect"

const longComputation = Effect.gen(function*() {
  for (let i = 0; i < 1000; i++) {
    // Do some work
    yield* Effect.sync(() => compute(i))
    
    // Check for interruption every 100 iterations
    if (i % 100 === 0) {
      yield* Effect.allowInterrupt
    }
  }
})

Disconnecting Interruption

disconnect

Run an effect in a separate interruption context:
import { Effect, Fiber } from "effect"

const program = Effect.gen(function*() {
  const fiber = yield* Effect.fork(
    Effect.gen(function*() {
      // This will run to completion even if parent is interrupted
      yield* Effect.disconnect(
        Effect.gen(function*() {
          yield* Effect.sleep("5 seconds")
          yield* Effect.log("Background task completed")
        })
      )
      
      yield* Effect.never
    })
  )
  
  yield* Effect.sleep("1 second")
  yield* Fiber.interrupt(fiber)
  
  // Fiber is interrupted, but disconnected task continues
})

Fork Daemon

forkDaemon

Create fibers that aren’t automatically interrupted:
import { Effect } from "effect"

const program = Effect.gen(function*() {
  // This fiber will outlive the parent
  yield* Effect.forkDaemon(
    Effect.gen(function*() {
      yield* Effect.sleep("10 seconds")
      yield* Effect.log("Daemon task completed")
    })
  )
  
  yield* Effect.log("Parent completing")
  // Parent completes, daemon continues
})

Interruption Best Practices

Graceful Shutdown

import { Effect, Fiber, Queue, Ref } from "effect"

const worker = (queue: Queue.Queue<string>, running: Ref.Ref<boolean>) =>
  Effect.gen(function*() {
    while (yield* Ref.get(running)) {
      const item = yield* Queue.take(queue).pipe(
        Effect.timeout("1 second")
      )
      
      if (item._tag === "Some") {
        yield* processItem(item.value)
      }
    }
    
    yield* Effect.log("Worker shutting down gracefully")
  }).pipe(
    Effect.onInterrupt(() =>
      Effect.gen(function*() {
        yield* Ref.set(running, false)
        yield* Effect.log("Worker interrupted, draining queue...")
      })
    )
  )
Always use Effect.acquireUseRelease or Effect.addFinalizer for resource cleanup. These ensure cleanup runs even during interruption.

Testing Interruption

import { Effect, Fiber, Ref, TestClock } from "effect"
import { it } from "@effect/vitest"
import { assertTrue } from "@effect/vitest/utils"

it.effect("should handle interruption", () =>
  Effect.gen(function*() {
    const interrupted = yield* Ref.make(false)
    
    const fiber = yield* Effect.fork(
      Effect.never.pipe(
        Effect.onInterrupt(() => Ref.set(interrupted, true))
      )
    )
    
    yield* Fiber.interrupt(fiber)
    
    const result = yield* Ref.get(interrupted)
    assertTrue(result)
  })
)

Build docs developers (and LLMs) love