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

FunctionWhat it doesWhen to use it
Effect.succeed(value)Wraps a value in a successful EffectConstants, already-computed values
Effect.fail(error)Creates a failed EffectDomain errors, validation failures
Effect.sync(() => ...)Wraps a synchronous functionSide effects that can’t throw
Effect.try(() => ...)Wraps a sync function that might throwJSON.parse, SQLite queries
Effect.tryPromise(() => ...)Wraps a Promise that might rejectfetch, file I/O
Effect.async(resume => ...)Bridges callback-based APIsEvent emitters, process signals
Effect.sleep(duration)Delay executionTimeouts, 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.
FunctionWhat it doesWhen to use it
Effect.runPromise(effect)Execute and return a PromiseApplication entry point
Effect.runSync(effect)Execute synchronouslyMigrations, 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

FunctionWhat 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

FunctionWhat 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

FunctionWhat 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

Transforming

FunctionWhat 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:
ConceptPromise/async-awaitEffect
Async computationPromise<T>Effect<T, E, R>
Error handlingtry/catchType-tracked error channel
DependenciesModule imports / DI containerContext.Tag + Layer
Mutable statelet x = ...Ref<T>
Work queuehand-rolled with arraysQueue<T>
Background tasksetTimeout / detached promiseFiber via Effect.fork
One-shot signalnew Promise(resolve => ...)Deferred<T>
Type validationZod / JoiSchema
Retry logicmanual loopSchedule
Env configprocess.env.PORTConfig.number("PORT")
Secretsraw stringsRedacted<string>

Further Reading