Services
A service is a typed dependency: an interface plus a context key that lets
Effect find its implementation at runtime. In Effect v4 you declare one by
extending Context.Service, passing the class as the first type parameter and
the service’s shape as the second. By convention the implementation lives on a
static layer, so the whole service — interface, key, and how to build it —
stays in one file.
import { Context, Effect, Layer, Schema } from "effect"
// Errors are schema-defined tagged errors. `Schema.Defect` carries an unknown// underlying cause (e.g. a thrown driver error) in a typed field.export class DatabaseError extends Schema.TaggedErrorClass<DatabaseError>()( "DatabaseError", { cause: Schema.Defect }) {}
// The first type param is the class itself (the context key); the second is the// service's interface. The string id should be unique — prefix it with your// package and the file path.export class Database extends Context.Service<Database, { query(sql: string): Effect.Effect<ReadonlyArray<unknown>, DatabaseError>}>()("myapp/db/Database") { // The implementation is built lazily by a Layer. `Layer.effect` runs an // Effect once, when the layer is built, and stores the result as the service. static readonly layer = Layer.effect( Database, Effect.gen(function*() { // Functions that return Effects are written with `Effect.fn` so they get // a span/name — never a plain function that returns `Effect.gen(...)`. const query = Effect.fn("Database.query")(function*(sql: string) { yield* Effect.log(`Executing: ${sql}`) return [{ id: 1, name: "Alice" }, { id: 2, name: "Bob" }] })
// `Database.of` constructs an instance that satisfies the interface — it // gives you type checking on the object you return. return Database.of({ query }) }) )}Using the service is symmetric: yield* the class to get the implementation,
then call its methods. The requirement is tracked in the effect’s type until
the layer is provided.
import { Effect } from "effect"import { Database } from "./db/Database.ts"
const program = Effect.gen(function*() { const db = yield* Database const users = yield* db.query("SELECT * FROM users") yield* Effect.log(`Found ${users.length} users`)})
// `program` has type Effect<void, DatabaseError, Database>. Provide the layer to// discharge the `Database` requirement, then run it.Effect.runFork(program.pipe(Effect.provide(Database.layer)))How it works
Section titled “How it works”- The class is the key.
Context.Service<Self, Shape>()("id")returns a class whose runtime identity is the stringid. Two services with the same id occupy the same slot in the context, so keep ids unique. - The class is also an Effect. A service key is a yieldable
Effect<Shape, never, Self>— that is what makesyield* Databasework. It reaches into the surrounding context and returns the stored implementation. If noDatabasewas provided, the program won’t type-check at the point you try to run it. Selfis repeated inContext.Service<Self, Shape>()(...)because the class refers to itself as its own identifier type. The first type argument is the class you are defining; the empty()then takes the string id (and optionalmake). This two-stage call exists so TypeScript can infer the id as a literal type.Database.of(impl)is an identity function that exists purely for type inference — it checksimplagainst the declared interface.
Accessing a service without a generator
Section titled “Accessing a service without a generator”Outside of Effect.gen, two helpers let you reach a service inline. They return
an effect that still carries the service requirement:
import { Effect } from "effect"import { Database } from "./db/Database.ts"
// `use` for an effectful access...const rows = Database.use((db) => db.query("SELECT 1"))
// ...`useSync` when the accessor returns a plain value, not an effect.const queryFn = Database.useSync((db) => db.query)Services that depend on other services
Section titled “Services that depend on other services”A service’s layer is an ordinary Effect, so it can yield* other services it
needs. Those become requirements of the layer, which you satisfy when you
compose it — see Layer composition.
import { Context, Effect, Layer } from "effect"import { Database } from "./db/Database.ts"
export class UserRepo extends Context.Service<UserRepo, { countUsers: Effect.Effect<number>}>()("myapp/UserRepo") { static readonly layer = Layer.effect( UserRepo, Effect.gen(function*() { // Depending on Database here makes `Database` a requirement of this layer. const db = yield* Database
const countUsers = Effect.gen(function*() { const rows = yield* db.query("SELECT * FROM users") return rows.length })
return UserRepo.of({ countUsers }) }) // `Layer.provide` feeds Database in, so the resulting layer needs nothing. ).pipe(Layer.provide(Database.layer))}The three forms of Context.Service
Section titled “The three forms of Context.Service”Context.Service<Self, Shape>()(...) accepts three shapes of arguments. The
first is the common case shown above; the other two let you attach the build
effect directly to the class as a static make, which you then hand to
Layer.effect.
The id with no options. There is no built-in layer or make — you define
the implementation separately (the convention is a static layer). This is the
form used in every example above.
import { Context, Effect } from "effect"
export class Clock extends Context.Service<Clock, { now: Effect.Effect<number>}>()("myapp/Clock") {}
// You provide the implementation later, e.g. with Layer.succeed / Layer.effect,// or store it on a static `layer` as a convention.Pass { make } where make is an Effect. The class gains a static make
property holding that effect; build the layer with Layer.effect.
import { Context, Effect, Layer } from "effect"
export class Clock extends Context.Service<Clock, { now: () => number}>()("myapp/Clock", { // `make` is the effect that builds the implementation. make: Effect.succeed({ now: () => Date.now() })}) { // Build a layer from the attached `make` effect. static readonly layer = Layer.effect(Clock, Clock.make)}Pass { make } where make is a function (...args) => Effect. The static
make becomes that factory, so callers parameterize how the service is built.
import { Context, Effect, Layer } from "effect"
export class Greeter extends Context.Service<Greeter, { hi: () => string}>()("myapp/Greeter", { // A factory: arguments parameterize the build effect. make: (name: string) => Effect.succeed({ hi: () => `hi ${name}` })}) { // Call the factory to get the build effect, then wrap it in a layer. static readonly layer = (name: string) => Layer.effect(Greeter, Greeter.make(name))}Service members reference
Section titled “Service members reference”These members live on the class returned by Context.Service. Each is also part
of the Context.Service / Context.Key interfaces.
The build effect (or factory) you passed via the make option. Absent unless
you provided it. Feed it to a Layer.* constructor.
import { Context, Effect, Layer } from "effect"
class Config extends Context.Service<Config, { port: number }>()("Config", { make: Effect.succeed({ port: 8080 })}) {}
const layer = Layer.effect(Config, Config.make)// => Layer<Config>.layer
Section titled “.layer”Not a built-in member — it is the convention of attaching the layer as a
static property. Define it yourself; it is what Effect.provide consumes.
import { Context, Effect, Layer } from "effect"
class Config extends Context.Service<Config, { port: number }>()("Config") { static readonly layer = Layer.succeed(Config, { port: 8080 })}
Effect.runSync(Effect.provide(Config, Config.layer)).port// => 8080Service.of(impl)
Section titled “Service.of(impl)”Identity helper: returns impl unchanged, but type-checks it against the
service interface. Use it when constructing the implementation object so a wrong
shape is a compile error.
import { Context } from "effect"
class Cache extends Context.Service<Cache, { get: (k: string) => string}>()("Cache") {}
const impl = Cache.of({ get: (k) => `v:${k}` })// => { get: (k: string) => string } (now checked against the interface)Service.context(impl)
Section titled “Service.context(impl)”Builds a one-service Context from an implementation — a shortcut for
Context.make(Service, impl).
import { Context } from "effect"
class Cache extends Context.Service<Cache, { get: (k: string) => string}>()("Cache") {}
const ctx = Cache.context({ get: (k) => `v:${k}` })Context.get(ctx, Cache).get("a")// => "v:a"Service.use(f)
Section titled “Service.use(f)”Retrieves the service from the current context and applies an effectful
function f, in one step. The result keeps the service requirement.
import { Context, Effect } from "effect"
class Cache extends Context.Service<Cache, { get: (k: string) => Effect.Effect<string>}>()("Cache") {}
const value = Cache.use((c) => c.get("x"))// => Effect<string, never, Cache>Service.useSync(f)
Section titled “Service.useSync(f)”Like use, but f returns a plain value rather than an effect. Useful for
pulling out a method or a field.
import { Context } from "effect"
class Cache extends Context.Service<Cache, { get: (k: string) => string}>()("Cache") {}
const getter = Cache.useSync((c) => c.get)// => Effect<(k: string) => string, never, Cache>The string identifier you passed in. This is the runtime slot the service occupies in a context.
import { Context } from "effect"
class Cache extends Context.Service<Cache, { get: () => string }>()("myapp/Cache") {}
Cache.key// => "myapp/Cache"["Service"] and ["Identifier"]
Section titled “["Service"] and ["Identifier"]”Type-only fields on the key carrying the service shape and identifier. Index into the class type to recover the interface without redeclaring it.
import { Context, Effect } from "effect"
class Database extends Context.Service<Database, { query(sql: string): Effect.Effect<ReadonlyArray<unknown>>}>()("Database") {}
type DatabaseService = Database["Service"]// => { query(sql: string): Effect.Effect<ReadonlyArray<unknown>> }Manual context manipulation
Section titled “Manual context manipulation”Everything below operates on the Context value and service keys directly. You
rarely need these — Layer and Effect.provide build and thread the context
for you. They are useful for tests, adapters, and tooling that inspect or
assemble a context by hand. For building applications, prefer
layers.
For the entire section, assume these keys (a function-style key uses
Context.Service<Shape>("id") with a single type argument and no ()):
import { Context } from "effect"
const Port = Context.Service<{ PORT: number }>("Port")const Timeout = Context.Service<{ TIMEOUT: number }>("Timeout")Context.make(key, impl)
Section titled “Context.make(key, impl)”Creates a new Context holding a single service.
import { Context } from "effect"
const Port = Context.Service<{ PORT: number }>("Port")
const ctx = Context.make(Port, { PORT: 8080 })Context.get(ctx, Port)// => { PORT: 8080 }Context.empty()
Section titled “Context.empty()”The empty context — no services. Useful as a starting point or a default.
import { Context } from "effect"
Context.isContext(Context.empty())// => trueContext.add(self, key, impl)
Section titled “Context.add(self, key, impl)”Returns a new context with one more service added (replacing any existing entry for the same key). Pipeable.
import { Context, pipe } from "effect"
const Port = Context.Service<{ PORT: number }>("Port")const Timeout = Context.Service<{ TIMEOUT: number }>("Timeout")
const ctx = pipe( Context.make(Port, { PORT: 8080 }), Context.add(Timeout, { TIMEOUT: 5000 }))Context.get(ctx, Timeout)// => { TIMEOUT: 5000 }Context.get(self, key)
Section titled “Context.get(self, key)”Reads a service. The context type must prove the service is present, so this is the type-safe getter.
import { Context } from "effect"
const Port = Context.Service<{ PORT: number }>("Port")
Context.get(Context.make(Port, { PORT: 8080 }), Port)// => { PORT: 8080 }Context.getOption(self, key)
Section titled “Context.getOption(self, key)”Reads a service as an Option. Returns Option.none() when a regular key is
absent; for a Context.Reference returns Option.some of its default.
import { Context, Option } from "effect"
const Port = Context.Service<{ PORT: number }>("Port")const Timeout = Context.Service<{ TIMEOUT: number }>("Timeout")
const ctx = Context.make(Port, { PORT: 8080 })Context.getOption(ctx, Port) // => Option.some({ PORT: 8080 })Context.getOption(ctx, Timeout) // => Option.none()Context.getOrElse(self, key, orElse)
Section titled “Context.getOrElse(self, key, orElse)”Reads a service, evaluating orElse for a missing regular key. (A missing
Context.Reference resolves to its default instead of the fallback.)
import { Context } from "effect"
const Timeout = Context.Service<{ TIMEOUT: number }>("Timeout")
Context.getOrElse(Context.empty(), Timeout, () => ({ TIMEOUT: 1000 }))// => { TIMEOUT: 1000 }Context.getOrUndefined(self, key)
Section titled “Context.getOrUndefined(self, key)”Raw map lookup: returns the stored value or undefined. Does not resolve
Context.Reference defaults.
import { Context } from "effect"
const Port = Context.Service<{ PORT: number }>("Port")
Context.getOrUndefined(Context.empty(), Port)// => undefinedContext.merge(self, that)
Section titled “Context.merge(self, that)”Combines two contexts into one. On a key collision, that wins.
import { Context } from "effect"
const Port = Context.Service<{ PORT: number }>("Port")const Timeout = Context.Service<{ TIMEOUT: number }>("Timeout")
const ctx = Context.merge( Context.make(Port, { PORT: 8080 }), Context.make(Timeout, { TIMEOUT: 5000 }))Context.get(ctx, Timeout)// => { TIMEOUT: 5000 }Context.mergeAll(...ctxs)
Section titled “Context.mergeAll(...ctxs)”Merges a variadic list of contexts; the last context with a given key wins.
import { Context } from "effect"
const Port = Context.Service<{ PORT: number }>("Port")const Timeout = Context.Service<{ TIMEOUT: number }>("Timeout")
const ctx = Context.mergeAll( Context.make(Port, { PORT: 8080 }), Context.make(Timeout, { TIMEOUT: 5000 }))Context.get(ctx, Port)// => { PORT: 8080 }Context.pick(...keys)
Section titled “Context.pick(...keys)”Returns a context containing only the listed services (an allowlist). Pipeable.
import { Context, Option, pipe } from "effect"
const Port = Context.Service<{ PORT: number }>("Port")const Timeout = Context.Service<{ TIMEOUT: number }>("Timeout")
const ctx = pipe( Context.make(Port, { PORT: 8080 }), Context.add(Timeout, { TIMEOUT: 5000 }), Context.pick(Port))Context.getOption(ctx, Timeout)// => Option.none()Context.omit(...keys)
Section titled “Context.omit(...keys)”Returns a context with the listed services removed (a denylist). Pipeable.
import { Context, Option, pipe } from "effect"
const Port = Context.Service<{ PORT: number }>("Port")const Timeout = Context.Service<{ TIMEOUT: number }>("Timeout")
const ctx = pipe( Context.make(Port, { PORT: 8080 }), Context.add(Timeout, { TIMEOUT: 5000 }), Context.omit(Timeout))Context.getOption(ctx, Timeout)// => Option.none()Context.isContext(u)
Section titled “Context.isContext(u)”Type guard: is the value a Context? Does not check which services it holds.
import { Context } from "effect"
Context.isContext(Context.empty()) // => trueContext.isContext({}) // => falseContext.isKey(u)
Section titled “Context.isKey(u)”Type guard: is the value a service key (Context.Service or
Context.Reference)?
import { Context } from "effect"
Context.isKey(Context.Service("S")) // => trueContext.isKey(Context.empty()) // => falseContext.isReference(u)
Section titled “Context.isReference(u)”Type guard: is the value specifically a Context.Reference (a key with a
default value)?
import { Context } from "effect"
const LoggerRef = Context.Reference("Logger", { defaultValue: () => ({ log: (msg: string) => console.log(msg) })})
Context.isReference(LoggerRef) // => trueContext.isReference(Context.Service("S")) // => falseNext: give a service a default value with References, or learn the full set of layer constructors in Managing layers.