Effect TypeScript Best Practices
Effect is a TypeScript library for building complex, type-safe applications with structured
error handling, dependency injection via services/layers, fiber-based concurrency, and
resource safety.
When to Apply
- Writing or reviewing TypeScript code that imports from
effect, @effect/schema, or @effect/platform
- Implementing typed error handling with
Effect<Success, Error, Requirements>
- Building services and layers for dependency injection
- Working with Schema for data validation, decoding, and transformation
- Using fiber-based concurrency (queues, semaphores, PubSub, deferred)
- Processing data with Stream and Sink
- Migrating from Promises, fp-ts, neverthrow, or ZIO to Effect
How to Use
This skill is organized by domain. Read the relevant reference file for the area you're working in.
Read First: The Paradigm
Always read this before diving into API references, especially when refactoring existing
code to use Effect or writing new Effect services:
| Reference |
When to Read |
| Think in Effect: The Paradigm Shift |
Before any other reference. Mental model shifts, refactoring recipes, anti-patterns, application architecture. Read this to understand HOW to think in Effect — the other files teach WHAT to type. |
Core Foundations
| Reference |
When to Read |
| Getting Started |
Creating the Effect type, pipelines, generators, running effects |
| Error Management |
Typed errors, recovery, retrying, timeouts, sandboxing |
| Core Concepts |
Request batching, configuration management, runtime system |
Data & Validation
| Reference |
When to Read |
| Data Types |
Option, Either, Cause, Chunk, DateTime, Duration, Exit, Data |
| Schema Basics |
Schema intro, basic usage, classes, constructors, effect data types |
| Schema Advanced |
Transformations, filters, annotations, error formatting, JSON Schema output |
Architecture & Dependencies
Concurrency & Streaming
| Reference |
When to Read |
| Concurrency |
Fibers, Deferred, Latch, PubSub, Queue, Semaphore |
| Streams and Sinks |
Creating, consuming, transforming streams; sink operations |
| Scheduling |
Built-in schedules, cron, combinators, repetition |
Platform & Observability
Style, AI & Migration
| Reference |
When to Read |
| Code Style |
Branded types, pattern matching, dual APIs, guidelines, traits |
| AI Integration |
Effect AI packages for LLM tool use and execution planning |
| Micro |
Lightweight Effect alternative for smaller bundles |
| Migration Guides |
Coming from Promises, fp-ts, neverthrow, or ZIO |
Quick Reference — Common Patterns
The Effect Type
Effect<Success, Error, Requirements>
Creating Effects
import { Effect } from "effect"
const succeed = Effect.succeed(42)
const fail = Effect.fail(new Error("oops"))
const sync = Effect.try(() => JSON.parse(data))
const async = Effect.tryPromise(() => fetch(url))
const program = Effect.gen(function* () {
const user = yield* getUser(id)
const todos = yield* getTodos(user.id)
return { user, todos }
})
Running Effects
Effect.runPromise(program)
Effect.runPromiseExit(program)
Effect.runSync(program)
Typed Errors
import { Data, Effect } from "effect"
class NotFound extends Data.TaggedError("NotFound")<{
readonly id: string
}> {}
class Unauthorized extends Data.TaggedError("Unauthorized")<{}> {}
const getUser = (id: string) =>
Effect.gen(function* () {
})
Services and Layers
import { Context, Effect, Layer } from "effect"
class UserRepo extends Context.Tag("UserRepo")<
UserRepo,
{ readonly findById: (id: string) => Effect.Effect<User, NotFound> }
>() {}
const program = Effect.gen(function* () {
const repo = yield* UserRepo
return yield* repo.findById("1")
})
const UserRepoLive = Layer.succeed(UserRepo, {
findById: (id) => Effect.succeed({ id, name: "Alice" })
})
program.pipe(Effect.provide(UserRepoLive), Effect.runPromise)
Schema Validation
import { Schema } from "effect"
const User = Schema.Struct({
id: Schema.Number,
name: Schema.String,
email: Schema.String.pipe(Schema.pattern(/@/))
})
type User = typeof User.Type
const decode = Schema.decodeUnknownSync(User)
const user = decode({ id: 1, name: "Alice", email: "a@b.com" })
Pipelines
import { Effect, pipe } from "effect"
const result = pipe(
getTodos,
Effect.map((todos) => todos.filter((t) => !t.done)),
Effect.flatMap((active) => sendNotification(active.length)),
Effect.catchTag("NetworkError", () => Effect.succeed("offline"))
)
const result2 = getTodos.pipe(
Effect.map((todos) => todos.filter((t) => !t.done)),
Effect.flatMap((active) => sendNotification(active.length))
)
Gotchas
See gotchas.md for known failure points.