Why Effect?
Programming is hard, and most of the difficulty is not the happy path — it is everything around it: errors, dependencies, concurrency, cancellation, retries, and cleanup. In ordinary TypeScript these concerns live outside the type system, so the compiler can’t help you with them. Effect’s central insight is to bring them into the type system, where they become explicit, composable, and checked. The result is code that is easier to reason about, refactor, and test.
Consider a realistic service that fetches a user. Notice how the signature documents not just the result, but how the work can fail and what it depends on.
import { Context, Effect, Layer, Schema } from "effect"
// Errors are data: defined with Schema so they are typed, serializable, and// carry structured fields rather than an opaque message string.class UserNotFound extends Schema.TaggedErrorClass<UserNotFound>()( "UserNotFound", { id: Schema.Number }) {}
class User extends Schema.Class<User>("User")({ id: Schema.Number, name: Schema.String}) {}
// A service is declared as a class. Its shape is just an interface of effects.class Users extends Context.Service<Users, { readonly findById: (id: number) => Effect.Effect<User, UserNotFound>}>()("app/Users") { // The `layer` describes how to build the live implementation. static layer = Layer.sync(Users, () => { const store = new Map<number, User>([[1, new User({ id: 1, name: "Ada" })]])
return Users.of({ // `Effect.fn` is the idiomatic way to write a function that returns an // Effect: it names the function for stack traces and tracing spans. findById: Effect.fn("Users.findById")(function* (id: number) { const user = store.get(id) if (user === undefined) { // Returning a failure is just another value in the flow. return yield* new UserNotFound({ id }) } return user }) }) })}
// Business logic depends on `Users` without knowing the implementation.//// ┌─── the error is visible: UserNotFound// │ ┌─── the dependency is visible: Users// ▼ ▼// Effect<string, UserNotFound, Users>const greet = Effect.fn("greet")(function* (id: number) { const users = yield* Users const user = yield* users.findById(id) return `Hello, ${user.name}!`})Every important fact about greet is in its type: it succeeds with a string,
it can fail with UserNotFound, and it needs a Users service to run. None of
this is documentation that can drift out of date — it is checked by the
compiler. Below are the four pillars that make this possible.
Typed errors
Section titled “Typed errors”In typical TypeScript, a function either returns a value or throws. The throw is invisible to the type system, so it is easy to forget that a call can fail and easy to forget to handle it. As a codebase grows from one such function to a thousand, this becomes a real source of bugs.
Effect tracks errors in the E channel of Effect<A, E, R>. When you call
something that can fail, the failure shows up in your type, and the compiler
keeps it there until you handle it — with Effect.catch,
Effect.catchTag, or by recovering with a fallback. Errors are ordinary
tagged values (defined with Schema.TaggedErrorClass), so you can match on them
exhaustively and even serialize them across a network boundary.
Dependency injection
Section titled “Dependency injection”The greet function above asks for a Users service simply by yield*-ing it.
It never imports a concrete implementation, so the R channel records Users
as an unmet requirement. You satisfy that requirement by providing a Layer —
a recipe for constructing the service and its own dependencies:
import { Effect } from "effect"
// Providing `Users.layer` discharges the requirement: the resulting Effect has// `R = never` and is ready to run.const runnable = greet(1).pipe(Effect.provide(Users.layer))Because dependencies are values, you can provide a real database-backed Users
in production and an in-memory one in tests — without changing a single line of
business logic. The compiler guarantees you never forget to provide something:
an unprovided service is a type error, not a runtime crash. See
Services & Layers for the full story.
Structured concurrency
Section titled “Structured concurrency”Effect runs on a lightweight fiber runtime. Fibers are cheap, cooperatively scheduled green threads — you can spawn thousands of them. Crucially, Effect’s concurrency is structured: child fibers are tied to their parent’s lifetime, so when a parent finishes, fails, or is interrupted, its children are cleaned up automatically. There are no leaked tasks and no dangling work.
This gives you racing, timeouts, interruption, and bounded parallelism as first-class, composable operations — and resource acquisition/release that is guaranteed to run even under interruption. Explore this in Concurrency and Resource Management.
One ecosystem
Section titled “One ecosystem”The biggest practical payoff is that everything speaks the same language. In a typical project you might reach for separate libraries for retries, validation, streaming, caching, tracing, and HTTP — each with its own API and its own way of handling errors and async. Effect provides standardized, composable solutions for all of them under one umbrella:
- Scheduling and retries with composable policies
- Schema for parsing, validation, and serialization
- Streaming for pull-based, backpressured data
- Caching and Batching for efficiency
- Observability — tracing, logs, and metrics built in
- HTTP Client, HTTP API, RPC, SQL, CLI, and AI for building real systems
Because these tools all return Effect values, they compose with each other and
with your own code for free. You don’t have to adopt all of it at once — start
with the pieces that solve your immediate problem, and reach for more as you go.
Next, get a project set up in Installation, then build something real in the Quickstart.