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()})Branching inside a generator
Section titled “Branching inside a generator”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 })when - run an effect conditionally
Section titled “when - run an effect conditionally”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).
forEach - run an effect for each element
Section titled “forEach - run an effect for each element”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 })whileLoop - loop while a condition holds
Section titled “whileLoop - loop while a condition holds”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.
Looping a single effect
Section titled “Looping a single 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.
Choosing an approach
Section titled “Choosing an approach”- Branching - prefer native
if/elseinsideEffect.gen; useEffect.whento conditionally run an effect within a pipeline. - Validating a value - the filter family:
Effect.filterOrFailto fail on an invalid value (narrowing with a refinement),Effect.filterOrElseto recover with a fallback effect, andfilterMap*variants when aFilterboth checks and transforms. UseEffect.filter/Effect.filterMap/Effect.filterMapEffectto filter an iterable’s elements. - Iterating a collection -
Effect.forEach(withdiscard/concurrencyoptions as needed). - Looping a single effect - a
for/whileloop inEffect.genfor readability,Effect.whileLoopwhen you want a single composable effect,Effect.foreverto run until interruption,Effect.replicateEffectto run a fixed number of times and collect, andEffect.repeatfor policy-driven repetition.
For retrying and repeating effects on a policy, see Scheduling; for combining many effects, see Building pipelines.
Reference
Section titled “Reference”Filtering
Section titled “Filtering”filter
Section titled “filter”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 iterableconst evens = Effect.filter([1, 2, 3, 4], (n) => n % 2 === 0)// => Effect<number[]> -> [2, 4]
// Effectful predicateconst checked = Effect.filter([1, 2, 3], (n) => Effect.succeed(n > 1))// => Effect<number[]> -> [2, 3]filterMap
Section titled “filterMap”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"]filterMapEffect
Section titled “filterMapEffect”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]filterOrElse
Section titled “filterOrElse”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"filterMapOrElse
Section titled “filterMapOrElse”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"filterOrFail
Section titled “filterOrFail”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)filterMapOrFail
Section titled “filterMapOrFail”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"Conditional operators
Section titled “Conditional operators”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 nothingFor 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))).
Looping
Section titled “Looping”forEach
Section titled “forEach”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]whileLoop
Section titled “whileLoop”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 = 0const 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"Repetition
Section titled “Repetition”forever
Section titled “forever”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 interruptedreplicate
Section titled “replicate”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 runreplicateEffect
Section titled “replicateEffect”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 timesrepeat
Section titled “repeat”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).