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, iterating an effect over a collection, looping until a
condition holds, and turning a predicate into a failure.
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).
filterOrFail - turn a predicate into a failure
Section titled “filterOrFail - turn a predicate into a failure”Effect.filterOrFail checks the success value against a predicate and, when it
does not hold, fails with an error you construct. It is the effectful equivalent
of a guard clause, and it narrows 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 ) )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.
Choosing an approach
Section titled “Choosing an approach”- Branching - prefer native
if/elseinsideEffect.gen; useEffect.whento conditionally run an effect within a pipeline, andEffect.filterOrFailto fail on an invalid value. - Iterating a collection -
Effect.forEach(withdiscard/concurrencyoptions as needed). - Looping until a condition - a
for/whileloop inEffect.genfor readability, orEffect.whileLoopwhen you want a single composable effect.
For retrying and repeating effects on a policy, see Scheduling; for combining many effects, see Building pipelines.