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")
ChannelType paramDefaultMeaning
SuccessArequiredThe value produced on success
ErrorEneverThe error type (never = infallible)
RequirementsRneverContext 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 ParseError or NotFound
  • It requires a Database service 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.sync or Effect.promise don’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.