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

Basic Concurrency

Ordinary Effeck calls run in sequence. Use Effect's concurrency combinators to overlap work, choose a concurrency limit, and control cancellation. Pass functions describing the work so the combinator starts each operation at the right time.

Sequential, bounded, and unbounded work

function task(id: number) {
  logInfo("starting", id)
  sleep("40 millis")
  return id * 2
}

function runTasks() {
  const sequential = all([task(1), task(2)])
  const bounded = forEach([1, 2, 3, 4], (id) => task(id), {
    concurrency: 2
  })
  const unbounded = await.all [task(5), task(6), task(7)]
  return { sequential, bounded, unbounded }
}

The default is sequential execution. A positive numeric limit bounds active operations. await.all [...] starts every element in its own fiber at once, like Promise.all, and is Effect.all with unbounded concurrency. Results keep the order written even when work finishes out of order; choose a finite limit with forEach or for concurrent(n) when work uses a constrained service or resource.

Interrupting computations

function cancelWork() {
  const worker = fork {
    sleep("1 minute")
    return "finished"
  }
  worker.interrupt()
  return Fiber.await(worker)
}

Fiber.interrupt requests cancellation and waits for termination, including finalizers. Fiber.await then returns an Exit describing the interruption rather than propagating it. Effect.interrupt() interrupts the current fiber; interruption is outside the expected error channel and ordinary catch does not handle it.

Attaching interruption cleanup

function cancelWithCleanup() {
  const worker = fork(Effect.onInterrupt(() => {
    try {
      sleep("1 minute")
    } finally {
      logInfo("released worker resources")
    }
  }, () => logInfo("worker cancelled")))
  sleep("10 millis")
  worker.interrupt()
}

finally runs on every exit, while onInterrupt adds work specifically for cancellation. Cleanup runs before the interrupting caller resumes. For acquired resources, use using so release is registered as part of acquisition rather than depending on a later statement.

Propagating cancellation across concurrent work

function cancelBatch() {
  for concurrent (const id of [1, 2, 3]) {
    try {
      if (id === 2) {
        sleep("10 millis")
        Effect.interrupt()
      }
      sleep("1 second")
    } finally {
      logInfo("branch finished", id)
    }
  }
}

for concurrent runs each item in its own fiber. An interrupted branch stops the loop and interrupts its active siblings. An expected failure also stops the default concurrent traversal and cancels work whose result is no longer needed. The parent waits for branch cleanup; a cause containing interruption or a defect passes through ordinary catch intact.

Racing under different winner rules

function answer(label: string, delay: Duration.Input) {
  sleep(delay)
  return label
}

function raceExamples() {
  const firstSuccess = race(
    () => { throw "unavailable" },
    answer("replica", "20 millis")
  )
  const anySuccess = await.any [
    answer("west", "40 millis"),
    answer("east", "10 millis"),
    answer("central", "30 millis")
  ]
  const firstCompletion = await.race [
    answer("primary", "10 millis"),
    answer("backup", "50 millis")
  ]
  return { firstSuccess, anySuccess, firstCompletion }
}

The await operations come from the TC39 await operations proposal, and each behaves as the Promise method of the same name. await.any waits for a success (Effect.raceAll), so one early failure does not defeat another branch that can still succeed; if no branch succeeds, it fails with the collected failure reasons. await.race accepts the earliest completion, including failure (Effect.raceFirst). In both, the losing work is interrupted and cleaned up before the race returns. A named Effect function keeps Effect's meaning: race waits for a success, as Effect.race does.

Inspecting the winner

function inspectRace() {
  const winners: number[] = []
  const outcome = Effect.result(Effect.raceFirst(
    () => { throw "primary offline" },
    () => {
      sleep("1 second")
      return "backup response"
    },
    { onWinner: ({ index }) => { winners.push(index) } }
  ))
  if (outcome.isFailure()) {
    logWarning("first completion failed", outcome.failure)
  }
  return { outcome, winners }
}

The synchronous onWinner callback observes the winning fiber and index, zero for the first branch and one for the second. It does not replace the winning result and cannot run an Effeck operation that pauses. Effect.result captures expected failures as data; use Effect.exit when defects and interruption must also be represented.

Coming from Effect

Effect.all([task(1), task(2)], { concurrency: "unbounded" }) in TypeScript becomes await.all [task(1), task(2)] in Effeck, and Effect.raceFirst(a, b) becomes await.race [a(), b()]. Each element is passed by name, so task(1) starts in its own fiber instead of running before the operation.