Skip to content

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.

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

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)

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.

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.

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

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

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

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

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)

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

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

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)

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

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

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>

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)

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)

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)

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

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.

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])
APIInputOutput
mapEffect<A, E, R>, A => BEffect<B, E, R>
asEffect<A, E, R>, BEffect<B, E, R>
asVoidEffect<A, E, R>Effect<void, E, R>
flattenEffect<Effect<A, ...>, ...>Effect<A, ...>
flatMapEffect<A, E, R>, A => Effect<B, ...>Effect<B, ...>
andThenEffect<A, E, R>, effect or A => effectEffect<B, ...>
tapEffect<A, E, R>, A => Effect<X, ...>Effect<A, ...>
zipEffect<A, ...>, Effect<B, ...>Effect<[A, B], ...>
zipWithtwo effects + (A, B) => CEffect<C, ...>
all[Effect<A, ...>, Effect<B, ...>, ...]Effect<[A, B, ...], ...>
Do / bind / bindTorecord-building stepsEffect<{ ...named fields }, ...>
partitionIterable<A>, A => Effect<B, E, R>Effect<[Array<E>, Array<B>], never, R>
validateIterable<A>, A => Effect<B, E, R>Effect<Array<B>, NonEmptyArray<E>, R>
findFirstIterable<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.