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)))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. 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 }>You can run the effects concurrently with the { concurrency } option - see
Concurrency for the details.
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> |
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, ...> |
all | [Effect<A, ...>, Effect<B, ...>, ...] | Effect<[A, B, ...], ...> |
For branching and looping over effects, continue to Control flow.