Effect TLDR
This page is a condensed reference for every Effect concept used in Diminuendo. Each entry gives you the one-sentence explanation, a minimal code example, and a link to the official docs. Think of it as the cheat sheet you wish existed when you first opened the Effect website.
New to Effect? Start with Why Effect TS for the motivation, then come back here as a reference. The Effect TS Patterns page shows how Diminuendo uses these in practice.
Core Types
Effect<Success, Error, Requirements>
The fundamental type. Represents a lazy computation that may succeed with Success, fail with Error, or require services described by Requirements. Nothing runs until you explicitly execute it.
import { Effect } from "effect"
// A computation that succeeds with a number, can fail with a string, and needs no services
const program: Effect.Effect<number, string> = Effect.succeed(42)
Official docs →
Effect.gen
Generator-based syntax for writing sequential Effect code. yield* unwraps an Effect (like await for Promises, but type-safe). This is how you write most Effect code.
const program = Effect.gen(function* () {
const config = yield* AppConfig // resolve a service dependency
const db = yield* getDatabase(config) // chain another Effect
const users = yield* db.listUsers() // and another
return users.length // final success value
})
Official docs →
Effect.pipe
Left-to-right function composition. Chains transformations on an Effect without nested calls.
const result = pipe(
fetchUser(id),
Effect.map(user => user.name),
Effect.catchAll(() => Effect.succeed("anonymous")),
Effect.tap(name => Effect.log(`Resolved: ${name}`))
)
Official docs →
Creating Effects
| Function | What it does | When to use it |
|---|
Effect.succeed(value) | Wraps a value in a successful Effect | Constants, already-computed values |
Effect.fail(error) | Creates a failed Effect | Domain errors, validation failures |
Effect.sync(() => ...) | Wraps a synchronous function | Side effects that can’t throw |
Effect.try(() => ...) | Wraps a sync function that might throw | JSON.parse, SQLite queries |
Effect.tryPromise(() => ...) | Wraps a Promise that might reject | fetch, file I/O |
Effect.async(resume => ...) | Bridges callback-based APIs | Event emitters, process signals |
Effect.sleep(duration) | Delay execution | Timeouts, backoff, throttling |
// Effect.try — wraps sync code that can throw
const parseJson = (raw: string) =>
Effect.try({
try: () => JSON.parse(raw),
catch: (e) => new ParseError({ message: String(e) })
})
// Effect.tryPromise — wraps async code that can reject
const fetchData = (url: string) =>
Effect.tryPromise({
try: () => fetch(url).then(r => r.json()),
catch: (e) => new NetworkError({ message: String(e) })
})
Running Effects
Effects are lazy — nothing happens until you run them. These are the “escape hatches” that bridge Effect world to the outside.
| Function | What it does | When to use it |
|---|
Effect.runPromise(effect) | Execute and return a Promise | Application entry point |
Effect.runSync(effect) | Execute synchronously | Migrations, CLI tools |
// Typically used exactly once, at the entry point
const main = Effect.gen(function* () { /* ... */ })
Effect.runPromise(main).catch(console.error)
Avoid calling runPromise or runSync deep inside your code. The whole point of Effect is to compose computations and run them once at the boundary.
Error Handling
Data.TaggedError
Type-safe error classes with a _tag discriminator. Unlike thrown exceptions, these are tracked in the Effect type signature — the compiler forces you to handle them.
import { Data } from "effect"
class SessionNotFound extends Data.TaggedError("SessionNotFound")<{
readonly sessionId: string
}> {}
class DbError extends Data.TaggedError("DbError")<{
readonly message: string
readonly cause?: unknown
}> {}
// The error channel is visible in the type: Effect<Session, SessionNotFound | DbError>
Official docs →
Catching Errors
| Function | What it does |
|---|
Effect.catchAll(handler) | Catch all errors, must return a new Effect |
Effect.catchTag("Tag", handler) | Catch a specific tagged error by name |
Effect.matchEffect({ onSuccess, onFailure }) | Branch on success or failure |
Effect.orElseSucceed(fallback) | Recover from any error with a default value |
Effect.catchAllDefect(handler) | Catch unexpected panics (not typed errors) |
const safe = pipe(
fetchUser(id),
Effect.catchTag("NotFound", () => Effect.succeed(defaultUser)),
Effect.catchTag("DbError", (e) => Effect.fail(new ServiceUnavailable({ cause: e }))),
)
Dependency Injection
Context.Tag
A typed key that identifies a service interface. Think of it as a type-safe string key for a service registry.
import { Context } from "effect"
class AuthService extends Context.Tag("AuthService")<AuthService, {
readonly verify: (token: string) => Effect.Effect<Identity, Unauthenticated>
readonly isAdmin: (userId: string) => Effect.Effect<boolean>
}>() {}
Layer
A recipe for building a service. Layers declare what they provide and what they require. Effect resolves the full dependency graph at composition time.
import { Layer } from "effect"
// Layer.effect — build a service from an Effect
const AuthServiceLive = Layer.effect(
AuthService,
Effect.gen(function* () {
const config = yield* AppConfig // declare dependency
return {
verify: (token) => /* ... */,
isAdmin: (userId) => /* ... */,
}
})
)
// Layer.sync — build from a pure value (no dependencies)
const LoggerLive = Layer.sync(Logger, () => consoleLogger)
Composing Layers
| Function | What it does |
|---|
Layer.provide(layer, dependency) | Wire a dependency into a layer |
Layer.merge(a, b) | Combine two layers side by side |
Layer.mergeAll(a, b, c, ...) | Combine many layers |
// Wire 20+ services into one composable unit
const AppLayer = Layer.mergeAll(
AuthServiceLive,
BillingServiceLive,
TenantDbPoolLive,
/* ... 15 more ... */
).pipe(
Layer.provide(AppConfigLive)
)
Official docs →
Concurrency Primitives
Ref<A>
A mutable reference that is safe for concurrent access. Every read and update is atomic.
import { Ref } from "effect"
const counter = yield* Ref.make(0) // create with initial value
const current = yield* Ref.get(counter) // read
yield* Ref.set(counter, 10) // replace
yield* Ref.update(counter, (n) => n + 1) // atomic transform
const [old, _] = yield* Ref.modify(counter, (n) => // read-and-update
[n, n + 1]
)
Official docs →
Fiber
A lightweight virtual thread. Like a Promise, but supports cancellation, supervision, and composition. Effect.fork() starts a Fiber without blocking.
import { Fiber, Effect } from "effect"
const fiber = yield* Effect.fork(longRunningTask) // start in background
// ... do other work ...
const result = yield* Fiber.join(fiber) // wait for result
// forkDaemon — runs independently, survives parent scope
yield* Effect.forkDaemon(backgroundPoller)
Official docs →
Queue<A>
A bounded, async-safe work queue. Producers offer items, consumers take them. Backpressure is built in.
import { Queue } from "effect"
const q = yield* Queue.bounded<Task>(100) // bounded capacity
yield* Queue.offer(q, myTask) // enqueue (blocks if full)
const task = yield* Queue.take(q) // dequeue (blocks if empty)
const maybe = yield* Queue.poll(q) // non-blocking peek
Official docs →
Deferred<A>
A one-shot, write-once value. One fiber produces a result, another fiber waits for it. Like a typed, cancellation-aware Promise.resolve().
import { Deferred } from "effect"
const slot = yield* Deferred.make<TurnOutcome, TurnError>()
// Producer side (in another fiber):
yield* Deferred.succeed(slot, { status: "complete", tokens: 1500 })
// Consumer side (blocks until resolved):
const outcome = yield* Deferred.await(slot)
Official docs →
HashMap
An immutable, structurally-shared map. Unlike JavaScript Map, updates return new instances — safe to use inside Ref.update().
import { HashMap } from "effect"
const empty = HashMap.empty<string, Session>()
const updated = HashMap.set(empty, "sess-1", mySession)
const found = HashMap.get(updated, "sess-1") // Option<Session>
Schema
Runtime type validation that generates both TypeScript types and JSON decoders from a single definition. Replaces Zod/Joi with compile-time and runtime safety.
Defining Schemas
import { Schema } from "effect"
const UserSchema = Schema.Struct({
id: Schema.String,
name: Schema.String,
age: Schema.Number,
role: Schema.optional(Schema.Literal("admin", "member")),
tags: Schema.Array(Schema.String),
metadata: Schema.Record({ key: Schema.String, value: Schema.Unknown }),
})
// TypeScript type is inferred automatically
type User = typeof UserSchema.Type
Validating Data
// Decode unknown input — returns Either<ParseError, User>
const result = Schema.decodeUnknownEither(UserSchema)(jsonBody)
// Or decode as an Effect (fails with ParseError)
const user = yield* Schema.decodeUnknown(UserSchema)(jsonBody)
Discriminated Unions
Perfect for message protocols — each variant has a type tag:
const ClientMessage = Schema.Union(
Schema.Struct({ type: Schema.Literal("run_turn"), sessionId: Schema.String, text: Schema.String }),
Schema.Struct({ type: Schema.Literal("stop_turn"), sessionId: Schema.String }),
Schema.Struct({ type: Schema.Literal("list_sessions") }),
// ... 34 total message types
)
Official docs →
Config
Type-safe environment variable loading with defaults, validation, and secret handling.
import { Config } from "effect"
const port = Config.number("PORT").pipe(Config.withDefault(8080))
const host = Config.string("HOST").pipe(Config.withDefault("0.0.0.0"))
const debug = Config.boolean("DEBUG").pipe(Config.withDefault(false))
const apiKey = Config.redacted("API_KEY") // Redacted<string> — won't log
Official docs →
Redacted<A>
A wrapper that prevents secrets from being accidentally logged or serialized.
import { Redacted } from "effect"
const secret = Redacted.make("sk-live-abc123")
console.log(secret) // → <redacted>
const value = Redacted.value(secret) // → "sk-live-abc123" (explicit unwrap)
Scheduling & Retries
Schedule
Composable retry and repetition policies. Combine strategies with pipe.
import { Schedule } from "effect"
// Exponential backoff: 500ms → 1s → 2s, max 3 attempts, with jitter
const retryPolicy = Schedule.exponential("500 millis").pipe(
Schedule.jittered,
Schedule.compose(Schedule.recurs(3))
)
// Apply to any Effect
const resilient = Effect.retry(fetchFromPodium, retryPolicy)
Official docs →
Logging
Built-in structured logging that flows through the Effect context. No global logger imports needed.
yield* Effect.logInfo("Server started", { port: 8080 })
yield* Effect.logWarning("Connection slow", { latencyMs: 450 })
yield* Effect.logError("Request failed", { error: e.message })
yield* Effect.logDebug("Cache hit", { key: "user:42" })
Log levels: Trace < Debug < Info < Warning < Error < Fatal
Configure via Layer:
import { Logger, LogLevel } from "effect"
const LoggerLive = Logger.replace(Logger.defaultLogger, prettyLogger).pipe(
Layer.merge(Logger.minimumLogLevel(LogLevel.Info))
)
Official docs →
Control Flow
Iterating
// Process a collection, running each item as an Effect
yield* Effect.forEach(sessions, (s) => closeSession(s), { concurrency: 4 })
// Same but discard results
yield* Effect.forEach(ids, (id) => notify(id), { discard: true })
Combining
| Function | What it does |
|---|
Effect.all([a, b, c]) | Run effects, collect all results |
Effect.all([a, b, c], { concurrency: 3 }) | Same, but in parallel |
Effect.race(a, b) | First to complete wins |
Effect.zip(a, b) | Run sequentially, return tuple |
Effect.zipRight(a, b) | Run both, keep second result |
Effect.zipLeft(a, b) | Run both, keep first result |
| Function | What it does |
|---|
Effect.map(effect, fn) | Transform the success value |
Effect.flatMap(effect, fn) | Chain into another Effect |
Effect.tap(effect, fn) | Side-effect without changing the value |
Effect.as(effect, value) | Replace the success value |
Quick Comparison
If you’re coming from other ecosystems, here’s how Effect concepts map:
| Concept | Promise/async-await | Effect |
|---|
| Async computation | Promise<T> | Effect<T, E, R> |
| Error handling | try/catch | Type-tracked error channel |
| Dependencies | Module imports / DI container | Context.Tag + Layer |
| Mutable state | let x = ... | Ref<T> |
| Work queue | hand-rolled with arrays | Queue<T> |
| Background task | setTimeout / detached promise | Fiber via Effect.fork |
| One-shot signal | new Promise(resolve => ...) | Deferred<T> |
| Type validation | Zod / Joi | Schema |
| Retry logic | manual loop | Schedule |
| Env config | process.env.PORT | Config.number("PORT") |
| Secrets | raw strings | Redacted<string> |
Further Reading