Building Pipelines
Pipelines compose effects as a sequence of transformations, read left to right.
Where Effect.gen shines for multi-step
imperative logic, pipe shines for short transformations and for attaching
cross-cutting behaviour - logging, spans, retries - to an existing effect. The
two styles interoperate freely; you will use both.
import { Effect, pipe } from "effect"
// A function to apply a discount, which may fail.const applyDiscount = (total: number, rate: number) => rate === 0 ? Effect.fail("Discount rate cannot be zero" as const) : Effect.succeed(total - (total * rate) / 100)
const fetchAmount = Effect.succeed(100)
const program = pipe( fetchAmount, // Log the value without changing it. Effect.tap((amount) => Effect.log(`amount: ${amount}`)), // Run another effect that depends on the value. Effect.flatMap((amount) => applyDiscount(amount, 5)), // Transform the success value. Effect.map((discounted) => discounted + 1), // Replace the value with a formatted string. Effect.map((final) => `Final amount to charge: ${final}`))
Effect.runFork(program) // logs "amount: 100", produces "Final amount to charge: 96"pipe(value, f1, f2, ..., fn) feeds value into f1, the result into f2,
and so on - the same as fn(...f2(f1(value))) but readable top to bottom
instead of inside out. Every effect also has a .pipe method, so these two
are equivalent:
import { Effect, pipe } from "effect"
const a = pipe(Effect.succeed(1), Effect.map((n) => n + 1))const b = Effect.succeed(1).pipe(Effect.map((n) => n + 1))The combinators below are designed for pipe: called with their options they
return a function Effect => Effect. Each also has a data-first overload
(Effect.map(self, f)) if you prefer to pass the effect directly.
map - transform the success value
Section titled “map - transform the success value”Effect.map applies a plain function to the success value, producing a new
effect. The error and requirement channels are untouched. Effects are immutable;
map returns a new effect rather than mutating the original.
import { Effect } from "effect"
// ┌─── Effect<number>// ▼const doubled = Effect.succeed(21).pipe(Effect.map((n) => n * 2))as - replace the success value
Section titled “as - replace the success value”Effect.as(value) ignores the success value and replaces it with a constant.
Effect.asVoid is the common special case that discards the value entirely.
import { Effect } from "effect"
const ready = Effect.succeed(5).pipe(Effect.as("ready" as const))const done = Effect.log("saved").pipe(Effect.asVoid)flatMap - chain a dependent effect
Section titled “flatMap - chain a dependent effect”When the next step is itself an effect that depends on the previous value, use
Effect.flatMap. It runs the inner effect and flattens the result, so you never
end up with an Effect<Effect<...>>. The errors and requirements of both
effects combine in the type.
import { Effect } from "effect"
const applyDiscount = (total: number, rate: number) => rate === 0 ? Effect.fail("Discount rate cannot be zero" as const) : Effect.succeed(total - (total * rate) / 100)
// ┌─── Effect<number, "Discount rate cannot be zero">// ▼const program = Effect.succeed(100).pipe( Effect.flatMap((amount) => applyDiscount(amount, 5)))Make sure every effect you create inside flatMap is actually returned or
chained - an effect you build but ignore simply never runs.
andThen - run the next step, value-agnostic
Section titled “andThen - run the next step, value-agnostic”Effect.andThen sequences two steps where the second may or may not use the
first’s value. The second argument is either an effect or a function
returning an effect:
import { Effect } from "effect"
const fetchAmount = Effect.succeed(100)
// Function returning an effect (like flatMap)...const a = fetchAmount.pipe(Effect.andThen((amount) => Effect.succeed(amount * 2)))
// ...or a standalone effect to run next, ignoring the previous value.const b = fetchAmount.pipe(Effect.andThen(Effect.log("fetched")))Use flatMap when you specifically want the transformation-of-a-value reading;
use andThen when “do this, then do that” is the clearer intent.
tap - run a side effect, keep the value
Section titled “tap - run a side effect, keep the value”Effect.tap runs an effect for its side effect (logging, metrics, an audit
write) and then passes the original value through unchanged. If the tapped
effect fails, the whole chain fails.
import { Effect } from "effect"
const applyDiscount = (total: number, rate: number) => Effect.succeed(total - (total * rate) / 100)
const program = Effect.succeed(100).pipe( // Observe the amount without consuming it... Effect.tap((amount) => Effect.log(`Applying discount to: ${amount}`)), // ...`amount` is still available to the next step. Effect.flatMap((amount) => applyDiscount(amount, 5)))tap has sibling combinators that fire on the failure path -
tapError, tapCause, tapDefect - covered in
Catching errors.
all - combine multiple effects
Section titled “all - combine multiple effects”Effect.all runs a collection of effects and combines their results, preserving
the shape of the input - a tuple in gives a tuple out, a record in gives a
record out, and any other iterable in gives an array out. By default it runs
sequentially and short-circuits on the first failure.
import { Effect } from "effect"
const config = Effect.succeed({ host: "localhost", port: 8080 })const dbStatus = Effect.succeed("connected")
// ┌─── Effect<[{ host: string; port: number }, string]>// ▼const startup = Effect.all([config, dbStatus])
// With a record, the keys are preserved:const named = Effect.all({ config, dbStatus })// ▼ Effect<{ config: {...}; dbStatus: string }>Effect.all accepts an options object as its second argument:
import { Effect } from "effect"
const tasks = [Effect.succeed(1), Effect.succeed(2), Effect.succeed(3)]
// Run all effects concurrently instead of sequentially.const concurrent = Effect.all(tasks, { concurrency: "unbounded" })// => Effect<[number, number, number]>
// Discard the results - useful when you only care about side effects.const discarded = Effect.all(tasks, { discard: true })// => Effect<void>
// mode: "result" keeps every outcome instead of short-circuiting; each// element becomes a Result.const collected = Effect.all(tasks, { mode: "result" })// => Effect<[Result<number, never>, Result<number, never>, Result<number, never>]>See Concurrency options for the full set of
concurrency settings.
Do-notation
Section titled “Do-notation”Do-notation builds up an accumulating record through a pipe, without the
nesting you would get from chained flatMaps. Each step adds a named field that
later steps can read. It is the pipe-style counterpart to
Effect.gen: prefer gen for general
imperative logic, and do-notation when you specifically want to thread a growing
record through a pipeline.
import { Effect } from "effect"
const fetchUser = Effect.succeed({ id: 1, name: "Ada" })const fetchProfile = (userId: number) => Effect.succeed({ userId, bio: "Mathematician" })
const program = Effect.Do.pipe( // Bind an effect's value under a name. Effect.bind("user", () => fetchUser), // Later binds can depend on earlier fields. Effect.bind("profile", ({ user }) => fetchProfile(user.id)), // `let` adds a plain, synchronous value. Effect.let("settings", ({ user }) => ({ theme: "dark", owner: user.name })))// => Effect<{ user: {...}; profile: {...}; settings: {...} }>// succeeds with:// {// user: { id: 1, name: "Ada" },// profile: { userId: 1, bio: "Mathematician" },// settings: { theme: "dark", owner: "Ada" }// }The starting point of a do-notation pipeline: an effect whose success value is
the empty record {}, ready for fields to be added.
import { Effect } from "effect"
const program = Effect.Do// => Effect<{}>Runs an effect, possibly depending on the fields accumulated so far, and stores its success value under a new name. Errors and requirements of the bound effect combine into the result.
import { Effect } from "effect"
const program = Effect.Do.pipe( Effect.bind("x", () => Effect.succeed(2)), Effect.bind("y", ({ x }) => Effect.succeed(x + 1)))// => Effect<{ x: number; y: number }> (succeeds with { x: 2, y: 3 })Adds a computed plain value (no effect) to the record, derived from the fields already present.
import { Effect } from "effect"
const program = Effect.Do.pipe( Effect.bind("x", () => Effect.succeed(2)), Effect.let("doubled", ({ x }) => x * 2))// => Effect<{ x: number; doubled: number }> (succeeds with { x: 2, doubled: 4 })bindTo
Section titled “bindTo”Lifts an existing effect’s success value into a one-field record under the given
name - the typical way to start a do-notation pipeline from a value you
already have, instead of Do.
import { Effect } from "effect"
const program = Effect.succeed(5).pipe( Effect.bindTo("count"), Effect.let("doubled", ({ count }) => count * 2))// => Effect<{ count: number; doubled: number }> (succeeds with { count: 5, doubled: 10 })Collection helpers
Section titled “Collection helpers”A handful of combinators on Effect apply an effectful function across an
iterable with different error semantics from forEach (which short-circuits on
the first failure).
partition
Section titled “partition”Applies an effectful function to every element and splits the outcomes into
[excluded, satisfying] - failures first, successes second. It runs everything
and never fails.
import { Effect } from "effect"
const program = Effect.partition([0, 1, 2, 3], (n) => n % 2 === 0 ? Effect.fail(`${n} is even`) : Effect.succeed(n))// => Effect<[Array<string>, Array<number>], never>// succeeds with [ ["0 is even", "2 is even"], [1, 3] ]validate
Section titled “validate”Runs an effectful function on every element and accumulates all failures
instead of stopping at the first - the key difference from forEach. If any
element fails, the result fails with a NonEmptyArray of every error; otherwise
it succeeds with all the collected values.
import { Effect } from "effect"
const allGood = Effect.validate([1, 3, 5], (n) => n % 2 === 0 ? Effect.fail(`${n} is even`) : Effect.succeed(n))// => Effect<Array<number>, NonEmptyArray<string>> (succeeds with [1, 3, 5])
const someBad = Effect.validate([0, 1, 2, 3], (n) => n % 2 === 0 ? Effect.fail(`${n} is even`) : Effect.succeed(n))// => fails with BOTH errors: ["0 is even", "2 is even"]// (forEach would have failed with only "0 is even")Pass { discard: true } to validate every element but throw away the successes,
yielding Effect<void, NonEmptyArray<E>>:
import { Effect } from "effect"
const program = Effect.validate( [1, 3], (n) => (n % 2 === 0 ? Effect.fail(`${n} is even`) : Effect.succeed(n)), { discard: true })// => Effect<void, NonEmptyArray<string>> (succeeds with undefined)findFirst
Section titled “findFirst”Returns the first element whose effectful predicate yields true, wrapped in an
Option. Short-circuits as soon as a match is found.
import { Effect } from "effect"
const program = Effect.findFirst([1, 2, 3, 4], (n) => Effect.succeed(n > 2))// => Effect<Option<number>> (succeeds with Option.some(3))findFirstFilter
Section titled “findFirstFilter”Like findFirst, but the predicate is an effectful filter returning a
Result: the first Result.succeed both selects and
transforms the matching element, returned in Option.some.
import { Effect, Result } from "effect"
const program = Effect.findFirstFilter([1, 2, 3, 4], (n) => Effect.succeed(n > 2 ? Result.succeed(`found ${n}`) : Result.fail("too small")))// => Effect<Option<string>> (succeeds with Option.some("found 3"))Reference
Section titled “Reference”A scrollable tour of every sequencing, combining, and transforming combinator
on Effect. Common cases are above; this section fills in the rest.
Transforms the success value with a plain function, leaving the error and requirement channels untouched.
import { Effect } from "effect"
const program = Effect.succeed(21).pipe(Effect.map((n) => n * 2))// => Effect<number> (succeeds with 42)mapEager
Section titled “mapEager”Like map, but for an already-resolved success effect it applies the
function immediately rather than deferring it; pending effects fall back to
regular map. A micro-optimisation - reach for plain map unless you have a
reason.
import { Effect } from "effect"
const resolved = Effect.succeed(5)const mapped = Effect.mapEager(resolved, (n) => n * 2) // applied eagerly// => Effect<number> (succeeds with 10)Replaces the success value with a constant, ignoring the original.
import { Effect } from "effect"
const program = Effect.succeed(5).pipe(Effect.as("ready" as const))// => Effect<"ready">asSome
Section titled “asSome”Wraps the success value in Option.some, producing an Effect<Option<A>>.
import { Effect } from "effect"
const program = Effect.succeed(5).pipe(Effect.asSome)// => Effect<Option<number>> (succeeds with Option.some(5))asVoid
Section titled “asVoid”Discards the success value entirely, producing Effect<void>. Common for
fire-and-forget steps whose result you do not need.
import { Effect } from "effect"
const program = Effect.succeed(5).pipe(Effect.asVoid)// => Effect<void>flatten
Section titled “flatten”Collapses a nested Effect<Effect<A>> into Effect<A>. Handy when a previous
step produced an effect-of-an-effect that you now want to run.
import { Effect } from "effect"
const nested = Effect.succeed(Effect.succeed(42))// => Effect<Effect<number>>
const program = Effect.flatten(nested)// => Effect<number> (succeeds with 42)flatMap
Section titled “flatMap”Runs an effect that depends on the previous success value and flattens the result. The error and requirement channels of both effects combine.
import { Effect } from "effect"
const program = Effect.succeed(2).pipe( Effect.flatMap((n) => Effect.succeed(n + 1)))// => Effect<number> (succeeds with 3)flatMapEager
Section titled “flatMapEager”Like flatMap, but when the input effect is already resolved as a success it
chains immediately; pending effects fall back to regular flatMap. As with
mapEager, an optimisation rather than a default choice.
import { Effect } from "effect"
const program = Effect.succeed(2).pipe( Effect.flatMapEager((n) => Effect.succeed(n + 1)))// => Effect<number> (succeeds with 3)andThen
Section titled “andThen”Sequences a second step that may or may not use the first’s value. Accepts either an effect or a function returning an effect.
import { Effect } from "effect"
const program = Effect.succeed(1).pipe(Effect.andThen(Effect.succeed("done")))// => Effect<string> (succeeds with "done")Runs an effect for its side effect and passes the original success value through unchanged. If the tapped effect fails, the chain fails.
import { Effect } from "effect"
const program = Effect.succeed(42).pipe( Effect.tap((n) => Effect.log(`saw ${n}`)))// => Effect<number> (logs "saw 42", still succeeds with 42)For tapping the failure path, see tapError, tapCause, and tapDefect in
Catching errors.
Runs two effects and keeps both results as a tuple [A, B]. Sequential by
default; pass { concurrent: true } to run them at the same time.
import { Effect } from "effect"
const program = Effect.zip(Effect.succeed(1), Effect.succeed("hello"))// => Effect<[number, string]> (succeeds with [1, "hello"])
const concurrent = Effect.zip( Effect.succeed(1), Effect.succeed("hello"), { concurrent: true })// => Effect<[number, string]>zipWith
Section titled “zipWith”Runs two effects and combines their results with a function, returning a single
value instead of a tuple. Also accepts { concurrent: true }.
import { Effect } from "effect"
const program = Effect.zipWith( Effect.succeed(1), Effect.succeed("hello"), (n, s) => n + s.length)// => Effect<number> (succeeds with 6)Runs a collection of effects and combines their results, preserving the input
shape (tuple in / tuple out, record in / record out, iterable in / array out).
Short-circuits on the first failure unless mode: "result" is set.
import { Effect } from "effect"
const program = Effect.all([Effect.succeed(1), Effect.succeed("a")])// => Effect<[number, string]> (succeeds with [1, "a"])Options: { concurrency } (see
Concurrency options), { discard: true }
to return Effect<void>, and { mode: "result" } to collect every outcome
without short-circuiting.
forEach
Section titled “forEach”The iterable counterpart of all: applies an effectful function to each element
and collects the results in order. Covered in full on
Control flow.
import { Effect } from "effect"
const program = Effect.forEach([1, 2, 3], (n) => Effect.succeed(n * 2))// => Effect<Array<number>> (succeeds with [2, 4, 6])Cheatsheet
Section titled “Cheatsheet”| API | Input | Output |
|---|---|---|
map | Effect<A, E, R>, A => B | Effect<B, E, R> |
as | Effect<A, E, R>, B | Effect<B, E, R> |
asVoid | Effect<A, E, R> | Effect<void, E, R> |
flatten | Effect<Effect<A, ...>, ...> | Effect<A, ...> |
flatMap | Effect<A, E, R>, A => Effect<B, ...> | Effect<B, ...> |
andThen | Effect<A, E, R>, effect or A => effect | Effect<B, ...> |
tap | Effect<A, E, R>, A => Effect<X, ...> | Effect<A, ...> |
zip | Effect<A, ...>, Effect<B, ...> | Effect<[A, B], ...> |
zipWith | two effects + (A, B) => C | Effect<C, ...> |
all | [Effect<A, ...>, Effect<B, ...>, ...] | Effect<[A, B, ...], ...> |
Do / bind / bindTo | record-building steps | Effect<{ ...named fields }, ...> |
partition | Iterable<A>, A => Effect<B, E, R> | Effect<[Array<E>, Array<B>], never, R> |
validate | Iterable<A>, A => Effect<B, E, R> | Effect<Array<B>, NonEmptyArray<E>, R> |
findFirst | Iterable<A>, A => Effect<boolean, ...> | Effect<Option<A>, ...> |
For branching and looping over effects, continue to Control flow. To pull values from services inside a pipeline, see Accessing services.