Skip to content

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.

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>

These combinators pull values out of the current context.

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(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() 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(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)

These combinators discharge requirements — they remove a service from the R channel by supplying an implementation.

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 removed

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 services
const 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(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(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.

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(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"
// => 1

A 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(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 later
const 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!"
  • Defining services and their dependencies: Services.
  • Building and wiring layers (composition, scoped resources, memoization): Managing Layers.
  • Default values for services via Context.Reference: References.