Skip to content

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.

src/db/Database.ts
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.

src/main.ts
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)))
  • The class is the key. Context.Service<Self, Shape>()("id") returns a class whose runtime identity is the string id. 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 makes yield* Database work. It reaches into the surrounding context and returns the stored implementation. If no Database was provided, the program won’t type-check at the point you try to run it.
  • Self is repeated in Context.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 optional make). 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 checks impl against the declared interface.

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)

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

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.

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>

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
// => 8080

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)

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"

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>

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"

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>> }

Everything below operates on the Context value and service keys directly. You rarely need theseLayer 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")

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 }

The empty context — no services. Useful as a starting point or a default.

import { Context } from "effect"
Context.isContext(Context.empty())
// => true

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 }

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 }

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

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 }

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)
// => undefined

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 }

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 }

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

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

Type guard: is the value a Context? Does not check which services it holds.

import { Context } from "effect"
Context.isContext(Context.empty()) // => true
Context.isContext({}) // => false

Type guard: is the value a service key (Context.Service or Context.Reference)?

import { Context } from "effect"
Context.isKey(Context.Service("S")) // => true
Context.isKey(Context.empty()) // => false

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) // => true
Context.isReference(Context.Service("S")) // => false

Next: give a service a default value with References, or learn the full set of layer constructors in Managing layers.