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
// ┌─── Success type
// │ ┌─── Error type
// │ │ ┌─── Required dependencies
// ▼ ▼ ▼
Effect<Success, Error, Requirements>
Creating Effects
import { Effect } from "effect"
// From sync values
const succeed = Effect.succeed(42)
const fail = Effect.fail(new Error("oops"))
// From sync code that may throw
const sync = Effect.try(() => JSON.parse(data))
// From promises
const async = Effect.tryPromise(() => fetch(url))
// From generators (recommended for complex flows)
const program = Effect.gen(function* () {
const user = yield* getUser(id)
const todos = yield* getTodos(user.id)
return { user, todos }
})
Running Effects
// Async (returns Promise)
Effect.runPromise(program)
// With full Exit information
Effect.runPromiseExit(program)
// Sync (throws on async)
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")<{}> {}
// Error type is tracked: Effect<User, NotFound | Unauthorized>
const getUser = (id: string) =>
Effect.gen(function* () {
// ...
})
Services and Layers
import { Context, Effect, Layer } from "effect"
// Define a service
class UserRepo extends Context.Tag("UserRepo")<
UserRepo,
{ readonly findById: (id: string) => Effect.Effect<User, NotFound> }
>() {}
// Use in effects — adds to Requirements channel
const program = Effect.gen(function* () {
const repo = yield* UserRepo
return yield* repo.findById("1")
})
// Implement with a Layer
const UserRepoLive = Layer.succeed(UserRepo, {
findById: (id) => Effect.succeed({ id, name: "Alice" })
})
// Provide and run
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
// Decode (parse + validate)
const decode = Schema.decodeUnknownSync(User)
const user = decode({ id: 1, name: "Alice", email: "a@b.com" })
Pipelines
import { Effect, pipe } from "effect"
// Data-last (pipe style)
const result = pipe(
getTodos,
Effect.map((todos) => todos.filter((t) => !t.done)),
Effect.flatMap((active) => sendNotification(active.length)),
Effect.catchTag("NetworkError", () => Effect.succeed("offline"))
)
// Fluent (method style)
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.