The Effect Type
Effect<A, E, R> is the core type. It describes a computation that produces a success value of type A, may fail with an error of type E, and requires a context of type R.
Three Channels
import { Effect } from "effect"
// Success: number, Error: never, Requirements: never
const one: Effect.Effect<number> = Effect.succeed(42)
// Success: number, Error: string, Requirements: never
const risky: Effect.Effect<number, string> = Effect.fail("boom")
// Success: void, Error: never, Requirements: never
const log: Effect.Effect<void> = Effect.log("hello")
| Channel | Type param | Default | Meaning |
|---|---|---|---|
| Success | A | required | The value produced on success |
| Error | E | never | The error type (never = infallible) |
| Requirements | R | never | Context services needed (never = no deps) |
Why This Matters
Plain TypeScript loses error and dependency information at type level. A function async function divide(a, b): Promise<number> can throw anything, and you won’t know until runtime:
// Plain TypeScript: errors are invisible in the type
async function divide(a: number, b: number): Promise<number> {
if (b === 0) throw new Error("Division by zero")
return a / b
}
// Caller has no idea this can throw
const result = await divide(10, 0) // runtime Error
With Effect, errors and requirements are part of the signature:
import { Effect, Schema } from "effect"
class DivisionByZero extends Schema.TaggedError<DivisionByZero>()("DivisionByZero", {
dividend: Schema.Number
}) {}
// Success: number, Error: DivisionByZero, Requirements: never
const divide = (a: number, b: number): Effect.Effect<number, DivisionByZero> =>
b === 0
? Effect.fail(new DivisionByZero({ dividend: a }))
: Effect.succeed(a / b)
Now the caller’s type checker forces them to handle DivisionByZero or propagate it.
Reading Type Signatures
When you see Effect.Effect<string, ParseError | NotFound, Database>, read it as:
- On success, you get a
string - It can fail with
ParseErrororNotFound - It requires a
Databaseservice in the context
An effect with E = never cannot fail. An effect with R = never has no dependencies and can run without providing any services.
Laziness
Effects are descriptions, not executions. Creating an effect does nothing - you must run it:
const program = Effect.succeed(42) // nothing happened yet
const result = Effect.runSync(program) // now it runs, returns 42
This means you can compose, transform, and inspect effects before they run:
import { Effect } from "effect"
const program = Effect.succeed(1).pipe(
Effect.map((n) => n + 1),
Effect.map((n) => n * 10)
)
// Still hasn't run - just a description
const result = Effect.runSync(program) // 20
Gotcha: Because effects are lazy, side effects in
Effect.syncorEffect.promisedon’t execute until you run the effect. If you need something to happen immediately, you’re probably not modeling it correctly.
The Requirements Channel in Practice
The R channel tracks what services your effect needs. When you access a service inside Effect.gen, the service type flows into R:
import { Context, Effect } from "effect"
class Logger extends Context.Service<Logger, {
log: (msg: string) => Effect.Effect<void>
}>()("myapp/Logger") {}
// R = Logger - this effect needs a Logger to run
const program = Effect.gen(function*() {
const logger = yield* Logger
yield* logger.log("Hello")
})
// Must provide Logger before running
program.pipe(Effect.provide(LoggerLive))
See Generators and Yield for how to compose effects, and Dependency Injection for the full service pattern.