Skip to main content
Effect provides structured concurrency through fibers—lightweight threads managed by the Effect runtime. This guide covers how to fork fibers, combine operations in parallel, and manage concurrent workflows safely.

Understanding Fibers

Fibers are Effect’s unit of concurrency. Unlike OS threads, fibers are cheap to create (thousands can run simultaneously) and are automatically interrupted when their parent scope closes.

Key Properties

  • Lightweight: Fibers have minimal overhead and fast context switching
  • Structured: Child fibers are automatically tracked and cleaned up
  • Interruptible: Fibers respect interruption signals for graceful shutdown
  • Scoped: Fibers are bound to a scope and cleaned up when it closes

Forking Fibers

Create new fibers with the fork operations.

Effect.forkScoped

Fork a fiber that’s bound to the current scope:
import { Effect } from "effect"

const Worker = Effect.gen(function*() {
  yield* Effect.logInfo("Starting worker...")
  
  // Fork a background fiber that runs until the scope closes
  yield* Effect.forkScoped(Effect.gen(function*() {
    while (true) {
      yield* Effect.logInfo("Working...")
      yield* Effect.sleep("1 second")
    }
  }))
  
  yield* Effect.logInfo("Worker setup complete")
})

// The forked fiber is automatically interrupted when the scope closes

Effect.fork

Fork a fiber and get a handle to control it:
import { Effect, Fiber } from "effect"

const program = Effect.gen(function*() {
  // Fork a fiber and get a handle
  const fiber = yield* Effect.fork(
    Effect.gen(function*() {
      yield* Effect.sleep("2 seconds")
      return "completed"
    })
  )
  
  yield* Effect.logInfo("Fiber started, doing other work...")
  yield* Effect.sleep("1 second")
  
  // Wait for the fiber to complete
  const result = yield* Fiber.join(fiber)
  yield* Effect.logInfo("Result:", result)
})

Effect.forkDaemon

Fork a daemon fiber that runs independently of any scope:
import { Effect } from "effect"

const backgroundTask = Effect.gen(function*() {
  // Daemon fiber runs until explicitly interrupted or the runtime shuts down
  yield* Effect.forkDaemon(
    Effect.gen(function*() {
      while (true) {
        yield* Effect.logInfo("Background task running...")
        yield* Effect.sleep("5 seconds")
      }
    })
  )
})
Use forkDaemon sparingly. Daemon fibers bypass scope-based cleanup and can leak if not carefully managed.

Parallel Execution

Effect provides powerful combinators for running operations concurrently.

Effect.all

Run multiple effects in parallel and collect results:
import { Effect } from "effect"

const program = Effect.gen(function*() {
  // Run three effects concurrently and collect results as an array
  const [user, posts, comments] = yield* Effect.all([
    fetchUser(1),
    fetchPosts(1),
    fetchComments(1)
  ], { concurrency: "unbounded" })
  
  return { user, posts, comments }
})

// Or use an object for named results
const programNamed = Effect.gen(function*() {
  const result = yield* Effect.all({
    user: fetchUser(1),
    posts: fetchPosts(1),
    comments: fetchComments(1)
  }, { concurrency: "unbounded" })
  
  // result has type { user: User, posts: Post[], comments: Comment[] }
  return result
})

Concurrency Control

Control how many operations run simultaneously:
import { Effect } from "effect"

const userIds = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

// Run at most 3 fetches concurrently
const users = yield* Effect.all(
  userIds.map(id => fetchUser(id)),
  { concurrency: 3 }
)

// Run all at once (unlimited concurrency)
const usersUnbounded = yield* Effect.all(
  userIds.map(id => fetchUser(id)),
  { concurrency: "unbounded" }
)

// Run sequentially (concurrency: 1)
const usersSequential = yield* Effect.all(
  userIds.map(id => fetchUser(id)),
  { concurrency: 1 }
)

Effect.forEach

Map over a collection with concurrent execution:
import { Effect } from "effect"

const enrichedOrders = yield* Effect.forEach(
  orders,
  (order) => enrichOrder(order),
  { concurrency: 4 }
)

Effect.race

Run effects concurrently and return the first to complete:
import { Effect } from "effect"

const program = Effect.gen(function*() {
  // Race two API calls, use whichever completes first
  const result = yield* Effect.race([
    fetchFromPrimaryAPI(),
    fetchFromBackupAPI()
  ])
  
  return result
})

Effect.zip

Combine two effects concurrently:
import { Effect } from "effect"

const program = Effect.gen(function*() {
  // Run both effects concurrently
  const [user, settings] = yield* Effect.zip(
    fetchUser(1),
    fetchSettings(1)
  )
  
  return { user, settings }
})

// Or use zipWith to transform the results
const combined = Effect.zipWith(
  fetchUser(1),
  fetchSettings(1),
  (user, settings) => ({ ...user, settings })
)

Structured Concurrency Patterns

Background Tasks with Scopes

Create long-running background tasks that are automatically cleaned up:
import { Effect, Layer } from "effect"

const BackgroundTask = Layer.effectDiscard(Effect.gen(function*() {
  yield* Effect.logInfo("Starting background task...")
  
  yield* Effect.gen(function*() {
    while (true) {
      yield* Effect.sleep("5 seconds")
      yield* Effect.logInfo("Background task running...")
    }
  }).pipe(
    Effect.onInterrupt(() => Effect.logInfo("Background task interrupted")),
    Effect.forkScoped
  )
}))

// The background task runs until the layer scope closes

Racing with Timeout

Race an effect against a timeout:
import { Effect } from "effect"

const program = Effect.gen(function*() {
  const result = yield* Effect.race([
    longRunningOperation(),
    Effect.sleep("5 seconds").pipe(Effect.as("timeout"))
  ])
  
  if (result === "timeout") {
    yield* Effect.logWarning("Operation timed out")
  }
})

// Or use Effect.timeout
const withTimeout = longRunningOperation().pipe(
  Effect.timeout("5 seconds")
)

Parallel Error Handling

When running effects in parallel, the first error causes all other fibers to be interrupted:
import { Effect } from "effect"

const program = Effect.gen(function*() {
  try {
    const results = yield* Effect.all([
      task1, // succeeds
      task2, // fails immediately
      task3  // interrupted when task2 fails
    ], { concurrency: "unbounded" })
  } catch (error) {
    // Only task2's error is caught
    // task3 was interrupted and cleaned up
  }
})

Fiber Management

Joining Fibers

Wait for a fiber to complete:
import { Effect, Fiber } from "effect"

const program = Effect.gen(function*() {
  const fiber = yield* Effect.fork(someOperation)
  
  // Do other work...
  yield* otherWork
  
  // Wait for the fiber to complete
  const result = yield* Fiber.join(fiber)
})

Interrupting Fibers

Manually interrupt a fiber:
import { Effect, Fiber } from "effect"

const program = Effect.gen(function*() {
  const fiber = yield* Effect.fork(longRunningTask)
  
  // Do some work
  yield* Effect.sleep("1 second")
  
  // Changed our mind, interrupt the fiber
  yield* Fiber.interrupt(fiber)
})

Awaiting Fibers

Wait for a fiber without joining its result:
import { Effect, Fiber } from "effect"

const program = Effect.gen(function*() {
  const fiber = yield* Effect.fork(backgroundTask)
  
  // Wait for completion but ignore the result
  yield* Fiber.await(fiber)
  
  yield* Effect.logInfo("Fiber completed")
})

Advanced Patterns

Fiber-local State

Store state that’s specific to a fiber:
import { Effect, FiberRef } from "effect"

const requestId = FiberRef.unsafeMake("default-request-id")

const program = Effect.gen(function*() {
  // Set value for current fiber
  yield* FiberRef.set(requestId, "req-123")
  
  // Read value
  const id = yield* FiberRef.get(requestId)
  
  yield* Effect.logInfo("Request ID:", id)
})

Racing Multiple Effects

Race many effects and get the first success:
import { Effect } from "effect"

const program = Effect.gen(function*() {
  const result = yield* Effect.raceAll([
    fetchFromAPI1(),
    fetchFromAPI2(),
    fetchFromAPI3()
  ])
  
  // Returns the first successful result
  // All other fibers are interrupted
})

Parallel Validation

Validate multiple things concurrently:
import { Effect } from "effect"

const validateUser = Effect.gen(function*() {
  // Run all validations in parallel
  yield* Effect.all([
    validateEmail(user.email),
    validatePassword(user.password),
    validateUsername(user.username)
  ], { concurrency: "unbounded" })
  
  return user
})

Best Practices

Prefer forkScoped

Use Effect.forkScoped over Effect.fork when possible. Scoped fibers are automatically cleaned up, preventing leaks.

Control Concurrency

Always specify reasonable concurrency limits with Effect.all and Effect.forEach to prevent resource exhaustion.

Handle Interruptions

Use Effect.onInterrupt to register cleanup handlers for background tasks and long-running operations.

Avoid Daemon Fibers

Only use Effect.forkDaemon when you truly need a fiber that outlives its parent scope. Prefer scoped fibers.

Concurrency Patterns Comparison

// Run all effects, collect all results
const results = yield* Effect.all([
  effect1,
  effect2,
  effect3
], { concurrency: "unbounded" })
Use when: You need all results and all effects must succeed.

Testing Concurrent Code

Use TestClock to test time-dependent concurrent code:
import { assert, it } from "@effect/vitest"
import { Effect, Fiber } from "effect"
import { TestClock } from "effect/testing"

it.effect("tests concurrent timeout", () =>
  Effect.gen(function*() {
    const fiber = yield* Effect.forkChild(
      Effect.sleep(60_000).pipe(Effect.as("done"))
    )
    
    // Advance virtual time
    yield* TestClock.adjust(60_000)
    
    const result = yield* Fiber.join(fiber)
    assert.strictEqual(result, "done")
  }))

Next Steps

Streaming

Learn about concurrent stream processing

Resource Management

Manage resources safely in concurrent contexts

Build docs developers (and LLMs) love