Accessing Services
Every Effect carries a third type parameter — its requirements (the R in
Effect<A, E, R>). Those requirements are the services the effect needs in
order to run. This page is about the combinators that let you read those
services from inside Effect code and provide them at the edges of your
program.
This page does not cover how to define a service or build a Layer — that
material lives in Services and
Managing Layers. Here we assume a
service already exists and focus on accessing it.
The common case
Section titled “The common case”Define a tiny service with Context.Service, read it inside Effect.gen, and
provide it once at the edge with Effect.provide.
import { Context, Effect, Layer } from "effect"
// 1. Define a service: an identifier paired with its shape. Attach a static// `layer` that supplies the implementation (the idiomatic v4 pattern).class Greeter extends Context.Service<Greeter, { greet: (name: string) => string}>()("Greeter") { static readonly layer = Layer.succeed(Greeter)({ greet: (name) => `Hello, ${name}!` })}
// 2. Read the service inside a generator.// `yield* Greeter` is the idiomatic way to read it — the service value// "flows out" of the yield, and `Greeter` is added to the requirements.const program = Effect.gen(function*() { const greeter = yield* Greeter return greeter.greet("Effect")})// program: Effect<string, never, Greeter>
// 3. Provide the implementation at the edge, discharging the requirement.const runnable = program.pipe(Effect.provide(Greeter.layer))// runnable: Effect<string, never, never>
Effect.runPromise(runnable).then(console.log)// => "Hello, Effect!"The key idea: reading a service (yield* Greeter) adds it to the R channel;
providing it (Effect.provide) removes it. A program is runnable once R
has been narrowed to never.
// before provide after provide// Effect<string, never, Greeter> -> Effect<string, never, never>Reading from the environment
Section titled “Reading from the environment”These combinators pull values out of the current context.
Effect.service
Section titled “Effect.service”Effect.service(key) returns Effect<S, never, I> — the explicit form of
reading a service. It adds the service to the effect’s requirements.
import { Context, Effect } from "effect"
class Database extends Context.Service<Database>()("Database", { make: Effect.succeed({ query: (sql: string) => Effect.succeed(`rows for: ${sql}`) })}) {}
const program = Effect.gen(function*() { const db = yield* Effect.service(Database) return yield* db.query("SELECT * FROM users")})// => Effect<string, never, Database>yield* Database inside a generator is the idiomatic sugar for
yield* Effect.service(Database) — both produce the same effect. Reach for the
explicit Effect.service form when composing with .pipe outside a generator:
import { Effect } from "effect"
const greeting = Effect.service(Database).pipe( Effect.flatMap((db) => db.query("SELECT name FROM users LIMIT 1")))// => Effect<string, never, Database>Effect.serviceOption
Section titled “Effect.serviceOption”Effect.serviceOption(key) returns Effect<Option<S>> — it reads an optional
dependency. If the service is present you get Option.some(impl); if it is
absent you get Option.none(). Crucially, it does not add the service to
the requirements, so the effect stays runnable without it.
import { Context, Effect, Option } from "effect"
class Logger extends Context.Service<Logger>()("Logger", { make: Effect.succeed({ log: (msg: string) => Effect.sync(() => console.log(msg)) })}) {}
const program = Effect.gen(function*() { const maybeLogger = yield* Effect.serviceOption(Logger) if (Option.isSome(maybeLogger)) { yield* maybeLogger.value.log("Logger is available") } return "done"})// => Effect<string, never, never> (Logger is NOT a requirement)
// Without providing Logger, the optional branch is simply skipped:Effect.runPromise(program).then(console.log)// => "done"Use serviceOption for soft dependencies — telemetry, an optional cache — where
the program should degrade gracefully when the service is missing.
Effect.context
Section titled “Effect.context”Effect.context() returns Effect<Context<R>> — the whole context as a value.
Useful when you want to inspect, capture, or forward the entire environment.
import { Context, Effect } from "effect"
class Database extends Context.Service<Database>()("Database", { make: Effect.succeed({ query: (sql: string) => Effect.succeed(sql) })}) {}
const program = Effect.gen(function*() { const ctx = yield* Effect.context<Database>() // ctx is a Context<Database>; pull values out with Context.get const db = Context.get(ctx, Database) return yield* db.query("SELECT 1")})// => Effect<string, never, Database>Effect.contextWith
Section titled “Effect.contextWith”Effect.contextWith(f) maps over the current context, where f receives the
full Context<R> and returns an effect. Use it to branch on which services are
available before committing to a path.
import { Console, Context, Effect, Option } from "effect"
class Cache extends Context.Service<Cache>()("Cache", { make: Effect.succeed({ get: (key: string) => `cached:${key}` })}) {}
const program = Effect.contextWith((ctx: Context.Context<never>) => { const cache = Context.getOption(ctx, Cache) return Option.isSome(cache) ? Effect.succeed(cache.value.get("user:1")) : Console.log("no cache").pipe(Effect.as("fallback"))})
Effect.runPromise(program).then(console.log)// => "fallback" (Cache was never provided)Providing services and context
Section titled “Providing services and context”These combinators discharge requirements — they remove a service from the R
channel by supplying an implementation.
Effect.provide
Section titled “Effect.provide”Effect.provide(layerOrContext) is the one you reach for most. It accepts a
Layer (or array of layers), or a prebuilt Context, and removes the services
they supply from the effect’s requirements. Layers are memoized across provide
calls by default; pass { local: true } to rebuild the layer each time.
import { Context, Effect, Layer } from "effect"
class Database extends Context.Service<Database, { query: (sql: string) => Effect.Effect<string>}>()("Database") { static readonly layer = Layer.succeed(Database)({ query: (sql) => Effect.succeed(`rows for: ${sql}`) })}
const program = Effect.gen(function*() { const db = yield* Database return yield* db.query("SELECT * FROM users")})// => Effect<string, never, Database>
const runnable = program.pipe(Effect.provide(Database.layer))// => Effect<string, never, never>
Effect.runPromise(runnable).then(console.log)// => "rows for: SELECT * FROM users"Provide several layers at once by passing an array:
import { Effect, Layer } from "effect"
declare const DatabaseLive: Layer.Layer<unknown>declare const LoggerLive: Layer.Layer<unknown>declare const program: Effect.Effect<string, never, unknown>
const runnable = program.pipe( Effect.provide([DatabaseLive, LoggerLive]))// => requirements supplied by either layer are removedEffect.provideContext
Section titled “Effect.provideContext”Effect.provideContext(context) provides an already-built Context, fulfilling
all the service requirements contained in it at once. Use it when you have
assembled a Context value directly rather than through layers.
import { Context, Effect } from "effect"
class Logger extends Context.Service<Logger, { log: (msg: string) => void}>()("Logger") {}class Config extends Context.Service<Config, { name: string}>()("Config") {}
// Build a context holding multiple servicesconst context = Context.make(Logger, { log: console.log }).pipe( Context.add(Config, { name: "World" }))
const program = Effect.gen(function*() { const logger = yield* Logger const config = yield* Config logger.log(`Hello ${config.name}`)})
const runnable = program.pipe(Effect.provideContext(context))// => Effect<void, never, never>
Effect.runPromise(runnable)// => logs "Hello World"Effect.provideService
Section titled “Effect.provideService”Effect.provideService(key, impl) provides a single service value directly — no
Layer required. It removes exactly that one service from the requirements.
import { Console, Context, Effect } from "effect"
class Config extends Context.Service<Config, { apiUrl: string}>()("Config") {}
const fetchData = Effect.gen(function*() { const config = yield* Config yield* Console.log(`Fetching from: ${config.apiUrl}`) return "data"})
const runnable = fetchData.pipe( Effect.provideService(Config, { apiUrl: "https://api.example.com" }))// => Effect<string, never, never>
Effect.runPromise(runnable).then(console.log)// => logs "Fetching from: https://api.example.com"// => "data"Effect.provideServiceEffect
Section titled “Effect.provideServiceEffect”Effect.provideServiceEffect(key, acquire) provides a service whose
implementation is produced by an effect. The acquisition runs each time the
wrapped effect runs, and any failures in acquire are surfaced in the error
channel.
import { Console, Context, Effect } from "effect"
class Database extends Context.Service<Database, { query: (sql: string) => Effect.Effect<string>}>()("Database") {}
// The implementation is built effectfully (e.g. opening a connection)const createConnection = Effect.gen(function*() { yield* Console.log("connecting...") yield* Effect.sleep("10 millis") return { query: (sql: string) => Effect.succeed(`rows for: ${sql}`) }})
const program = Effect.gen(function*() { const db = yield* Database return yield* db.query("SELECT 1")})
const runnable = program.pipe( Effect.provideServiceEffect(Database, createConnection))// => Effect<string, never, never>
Effect.runPromise(runnable).then(console.log)// => logs "connecting..."// => "rows for: SELECT 1"For implementations that need finalization (closing the connection), prefer a
scoped Layer — see Managing Layers.
Locally overriding services
Section titled “Locally overriding services”Sometimes you want to swap a service implementation for one subtree of your program without changing how it is provided at the edge — for example, raising the log level or bumping a config value just for a critical section.
Effect.updateService
Section titled “Effect.updateService”Effect.updateService(key, f) transforms the implementation of an
already-present service for the wrapped effect. The service must still be
provided somewhere upstream — updateService keeps it in the requirements.
import { Console, Context, Effect } from "effect"
class Counter extends Context.Service<Counter, { count: number}>()("Counter") {}
const program = Effect.gen(function*() { const counter = yield* Counter yield* Console.log(`count: ${counter.count}`) return counter.count}).pipe( // bump the count just for this effect Effect.updateService(Counter, (c: { count: number }) => ({ count: c.count + 1 })))
const runnable = program.pipe(Effect.provideService(Counter, { count: 0 }))
Effect.runPromise(runnable).then(console.log)// => logs "count: 1"// => 1A realistic use: override the logger for one noisy subtree while the rest of the app keeps the default.
import { Console, Context, Effect } from "effect"
class Logger extends Context.Service<Logger, { log: (m: string) => Effect.Effect<void>}>()("Logger") {}
const subtree = Effect.gen(function*() { const logger = yield* Logger yield* logger.log("inside the verbose section")})
const verbose = subtree.pipe( Effect.updateService(Logger, (base: { log: (m: string) => Effect.Effect<void> }) => ({ log: (m: string) => base.log(`[verbose] ${m}`) })))// `verbose` still requires Logger; provide it once at the edge.Effect.updateContext
Section titled “Effect.updateContext”Effect.updateContext(f) transforms the entire context for the wrapped
effect. The function receives the downstream Context and returns a new one,
letting you add or replace several services at once. This is how you can satisfy
part of the requirements inline while leaving the rest to be provided later.
import { Context, Effect } from "effect"
class Logger extends Context.Service<Logger, { log: (msg: string) => void}>()("Logger") {}class Config extends Context.Service<Config, { name: string}>()("Config") {}
const program = Effect.service(Config).pipe( Effect.map((config) => `Hello ${config.name}!`))// => Effect<string, never, Config>
// Provide Config by transforming the context, keeping Logger to be provided laterconst configured = program.pipe( Effect.updateContext((ctx: Context.Context<Logger>) => Context.add(ctx, Config, { name: "World" }) ))// => Effect<string, never, Logger> (Config supplied, Logger still required)
const runnable = configured.pipe( Effect.provideService(Logger, { log: console.log }))
Effect.runPromise(runnable).then(console.log)// => "Hello World!"Where to go next
Section titled “Where to go next”- Defining services and their dependencies: Services.
- Building and wiring layers (composition, scoped resources, memoization): Managing Layers.
- Default values for services via
Context.Reference: References.