Skip to content

Control Flow

JavaScript already has if, for, and while, and inside Effect.gen you should use them freely - they are the simplest way to branch and loop. Effect adds a handful of combinators for the cases where ordinary control flow does not fit a pipeline: running an effect conditionally, filtering a value through a predicate, iterating an effect over a collection, and looping a single effect (until a condition, forever, or a fixed number of times).

import { Effect, Option } from "effect"
// Inside a generator, plain `if` works as you'd expect. A function that
// returns an effect is written with `Effect.fn`.
const validateWeight = Effect.fn("validateWeight")(function*(weight: number) {
if (weight >= 0) {
return Option.some(weight)
}
yield* Effect.logWarning(`Rejecting negative weight: ${weight}`)
return Option.none()
})

Most branching just uses native syntax. Return from the success channel, or fail through the error channel - whichever models your domain:

import { Effect } from "effect"
const validateWeightOrFail = Effect.fn("validateWeightOrFail")(
function*(weight: number) {
if (weight < 0) {
// Fail with a typed error.
return yield* Effect.fail(`negative input: ${weight}` as const)
}
// Succeed with the valid value.
return weight
}
)

Effect.when runs an effect only when a condition effect evaluates to true. The result is wrapped in an Option: Some if the effect ran, None if it was skipped. This is the pipeline-friendly counterpart to an if statement.

import { Effect, Random } from "effect"
// Roll a die, but only log it when a coin flip comes up heads.
// ┌─── Effect<Option<number>>
// ▼
const program = Random.nextInt.pipe(
Effect.tap((n) => Effect.log(`rolled ${n}`)),
// The second argument is the condition - itself an effect.
Effect.when(Random.nextBoolean)
)
Effect.runFork(program)

The condition is an effect, so it can be effectful itself (a feature flag lookup, a random value, a config read). When you already have a plain boolean, wrap it with Effect.succeed(condition):

import { Console, Effect } from "effect"
const shouldLog = true
// ┌─── Effect<Option<void>>
// ▼
const program = Console.log("Condition is true!").pipe(
// A plain boolean becomes a condition effect via Effect.succeed.
Effect.when(Effect.succeed(shouldLog))
)
// => logs "Condition is true!" and yields Option.some(undefined)

The filter family - keep, transform, or reject a value

Section titled “The filter family - keep, transform, or reject a value”

The filter* combinators apply a predicate (or a Filter) to a value. They differ on two axes: whether they operate on an iterable’s elements or on a single effect’s success value, and on what happens when the check fails (drop, fall back to another effect, or fail the effect).

The most common one is filterOrFail - a guard clause that fails when the value is invalid, narrowing the type when you pass a refinement:

import { Effect } from "effect"
class TooSmall {
readonly _tag = "TooSmall"
constructor(readonly value: number) {}
}
const requirePositive = (n: number) =>
Effect.succeed(n).pipe(
Effect.filterOrFail(
(x) => x > 0, // keep the value when this is true
(x) => new TooSmall(x) // otherwise fail with this error
)
)

The full family is enumerated in the reference below (filter, filterMap, filterMapEffect, filterOrElse, filterMapOrElse, filterOrFail, filterMapOrFail).

Effect.forEach applies an effectful function to every element of an iterable and collects the results into an array, preserving order. It short-circuits on the first failure. The callback also receives the index.

import { Effect } from "effect"
const program = Effect.forEach([1, 2, 3, 4, 5], (n, index) =>
Effect.log(`at index ${index}`).pipe(Effect.as(n * 2))
)
// ▼ Effect<number[]> -> [2, 4, 6, 8, 10]

Pass { discard: true } when you only care about the side effects and want to skip building the result array (the effect then returns void). Pass a { concurrency } option to run the iterations concurrently - see Concurrency.

import { Effect } from "effect"
// Run for every element, ignore the results.
const logAll = Effect.forEach(
["a", "b", "c"],
(item) => Effect.log(item),
{ discard: true }
)

Effect.whileLoop repeatedly runs a body effect as long as a while condition returns true, calling step with each value the body produces. The while and step callbacks are plain (synchronous) functions; only body is an effect. It is the effectful analogue of a while loop, useful for draining a queue or polling until done.

import { Effect } from "effect"
const program = Effect.gen(function*() {
const seen: Array<number> = []
let counter = 0
yield* Effect.whileLoop({
// Keep going while this returns true.
while: () => counter < 5,
// The effect to run each iteration; here it increments the counter.
body: () => Effect.sync(() => ++counter),
// Called with each value the body produced - a plain `void` callback.
step: (n) => seen.push(n)
})
return seen // [1, 2, 3, 4, 5]
})

For state that you accumulate across iterations, a plain for/while loop inside Effect.gen (with let bindings and yield*) is often the most readable choice - reach for whileLoop when you want the loop itself expressed as a single composable effect.

To repeat one effect (rather than iterate a collection), use forever, replicate/replicateEffect, or - for policy-driven repetition with delays and limits - repeat on a Schedule. These are covered in the reference below.

  • Branching - prefer native if/else inside Effect.gen; use Effect.when to conditionally run an effect within a pipeline.
  • Validating a value - the filter family: Effect.filterOrFail to fail on an invalid value (narrowing with a refinement), Effect.filterOrElse to recover with a fallback effect, and filterMap* variants when a Filter both checks and transforms. Use Effect.filter / Effect.filterMap / Effect.filterMapEffect to filter an iterable’s elements.
  • Iterating a collection - Effect.forEach (with discard/concurrency options as needed).
  • Looping a single effect - a for/while loop in Effect.gen for readability, Effect.whileLoop when you want a single composable effect, Effect.forever to run until interruption, Effect.replicateEffect to run a fixed number of times and collect, and Effect.repeat for policy-driven repetition.

For retrying and repeating effects on a policy, see Scheduling; for combining many effects, see Building pipelines.

Filters the elements of an iterable, keeping only those that satisfy a predicate, refinement, or effectful predicate. A refinement narrows the element type; an effectful predicate may take a { concurrency } option.

import { Effect } from "effect"
// Sync predicate over an iterable
const evens = Effect.filter([1, 2, 3, 4], (n) => n % 2 === 0)
// => Effect<number[]> -> [2, 4]
// Effectful predicate
const checked = Effect.filter([1, 2, 3], (n) => Effect.succeed(n > 1))
// => Effect<number[]> -> [2, 3]

Filters and maps iterable elements with a Filter: each Result.succeed value is collected, each Result.fail value is dropped.

import { Effect, Filter, Option } from "effect"
// A Filter that passes positive numbers, doubling them on the way through.
const positiveDoubled = Filter.fromPredicateOption((n: number) =>
n > 0 ? Option.some(n * 2) : Option.none()
)
const doubled = Effect.filterMap([-1, 2, -3, 4], positiveDoubled)
// => Effect<number[]> -> [4, 8]
// The built-in Filter.string keeps only string values out of a mixed array.
const onlyStrings = Effect.filterMap([1, "a", 2, "b"], Filter.string)
// => Effect<string[]> -> ["a", "b"]

Like filterMap, but the Filter is effectful (a FilterEffect). Accepts an optional { concurrency }; with concurrency, results arrive in completion order, not input order.

import { Effect, Result } from "effect"
// FilterEffect: returns an Effect of a Result. succeed keeps & maps, fail drops.
const program = Effect.filterMapEffect(
[1, 2, 3, 4],
(n) => Effect.succeed(n % 2 === 0 ? Result.succeed(n * 10) : Result.fail(n))
)
// => Effect<number[]> -> [20, 40]

Tests a single effect’s success value against a predicate or refinement; when it fails, runs an orElse effect (which receives the value) to produce a fallback.

import { Effect } from "effect"
const program = Effect.succeed(5).pipe(
Effect.filterOrElse(
(n) => n % 2 === 0, // keep when even
(n) => Effect.succeed(`Number ${n} is odd`) // otherwise recover
)
)
// => Effect<number | string> -> "Number 5 is odd"

Validates and transforms a single effect’s success value with a Filter; on Result.fail it passes the failure value to an orElse fallback effect.

import { Console, Effect, Filter } from "effect"
const program = Effect.succeed<unknown>(42).pipe(
Effect.filterMapOrElse(
Filter.string, // succeed only for strings
(notAString) => Console.log(`not a string: ${notAString}`).pipe(Effect.as("fallback"))
)
)
// => Effect<string> -> logs "not a string: 42", yields "fallback"

Tests a single effect’s success value against a predicate or refinement; when it fails, fails the effect with orFailWith(value). Omit orFailWith to fail with NoSuchElementError. A refinement narrows the success type.

import { Effect } from "effect"
// Refinement narrows `string | number` down to `number`.
const program = Effect.succeed<string | number>(7).pipe(
Effect.filterOrFail(
(x): x is number => typeof x === "number",
(x) => `expected number, got ${typeof x}`
)
)
// => Effect<number, string> -> succeeds with 7 (a number)

Validates and transforms a single effect’s success value with a Filter; on Result.fail it fails the effect with orFailWith(failureValue) (or NoSuchElementError when omitted).

import { Effect, Filter } from "effect"
const program = Effect.succeed<unknown>("hi").pipe(
Effect.filterMapOrFail(
Filter.string,
(notAString) => `not a string: ${String(notAString)}`
)
)
// => Effect<string, string> -> succeeds with "hi"

Runs the effect only when a condition effect succeeds with true, wrapping the result in Option.some; when the condition is false the effect is skipped and the result is Option.none. A failing condition propagates its failure.

import { Console, Effect } from "effect"
const program = Console.log("ran!").pipe(Effect.when(Effect.succeed(true)))
// => Effect<Option<void>> -> logs "ran!", yields Option.some(undefined)
const skipped = Console.log("nope").pipe(Effect.when(Effect.succeed(false)))
// => Effect<Option<void>> -> yields Option.none(), logs nothing

For a plain boolean, wrap it with Effect.succeed(bool). There is no Effect.unless in v4 - negate the condition instead (Effect.when(Effect.succeed(!bool))).

Runs an effectful function for every element of an iterable, collecting the results in order and short-circuiting on the first failure. Options: discard (return void instead of an array) and concurrency.

import { Effect } from "effect"
const program = Effect.forEach([1, 2, 3], (n) => Effect.succeed(n + 1))
// => Effect<number[]> -> [2, 3, 4]

Repeatedly runs body while the synchronous while predicate holds, threading each produced value through the synchronous step callback. Only body is an effect.

import { Effect } from "effect"
let n = 0
const program = Effect.whileLoop({
while: () => n < 3,
body: () => Effect.sync(() => n++),
step: (value) => console.log(`step ${value}`)
})
// => Effect<void> -> logs "step 0", "step 1", "step 2"

Repeats an effect endlessly - it only ends on failure or interruption, and its success type is never. Pass { disableYield: true } to skip the automatic fiber yield on each iteration (tighter loop, less cooperative).

import { Effect, Fiber } from "effect"
const tick = Effect.log("tick").pipe(Effect.delay("1 second"))
// Run forever in a child fiber, then interrupt after a while.
const program = Effect.gen(function*() {
const fiber = yield* Effect.forkChild(Effect.forever(tick))
yield* Effect.sleep("3 seconds")
yield* Fiber.interrupt(fiber)
})
// => logs "tick" roughly every second until interrupted

Returns an Array<Effect> of n identical copies of the effect. It does not run anything - it just builds the array, which you then hand to a collector such as Effect.all.

import { Effect } from "effect"
const effects = Effect.replicate(Effect.succeed(1), 3)
// => Array<Effect<number>> of length 3 (nothing has run yet)
const program = Effect.all(effects)
// => Effect<number[]> -> [1, 1, 1] when run

Runs the effect n times and collects the results, with Effect.all semantics. Pass { concurrency } to run the repetitions in parallel, or { discard: true } to ignore the results (the effect then returns void).

import { Effect } from "effect"
const program = Effect.replicateEffect(Effect.succeed(1), 3)
// => Effect<number[]> -> [1, 1, 1]
const discarded = Effect.replicateEffect(Effect.log("hi"), 3, { discard: true })
// => Effect<void> -> logs "hi" three times

Repeats a successful effect according to a Schedule (or an options object with while / until / times / schedule), stopping on the first failure. The source always runs once before the schedule is stepped.

import { Console, Effect, Schedule } from "effect"
const action = Console.log("success")
const policy = Schedule.addDelay(Schedule.recurs(2), () => Effect.succeed("100 millis"))
const program = Effect.repeat(action, policy)
// => logs "success" three times (initial run + 2 repetitions)

See Scheduling for the full set of schedule combinators and for retry (the failure-driven counterpart of repeat).