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, 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()
})

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).

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
)
)

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.

  • Branching - prefer native if/else inside Effect.gen; use Effect.when to conditionally run an effect within a pipeline, and Effect.filterOrFail to fail on an invalid value.
  • Iterating a collection - Effect.forEach (with discard/concurrency options as needed).
  • Looping until a condition - a for/while loop in Effect.gen for readability, or Effect.whileLoop when you want a single composable effect.

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