Skip to content

Services: `Context.Tag` → `Context.Service`

In v3, services were defined using Context.Tag, Context.GenericTag, Effect.Tag, or Effect.Service. In v4, all of these have been replaced by Context.Service.

The underlying runtime data structure is a typed map from service identifiers to their implementations.

v3: Context.GenericTag

import { Context } from "effect"
interface Database {
readonly query: (sql: string) => string
}
const Database = Context.GenericTag<Database>("Database")

v4: Context.Service (function syntax)

import { Context } from "effect"
interface Database {
readonly query: (sql: string) => string
}
const Database = Context.Service<Database>("Database")

v3: Context.Tag class syntax

import { Context } from "effect"
class Database extends Context.Tag("Database")<Database, {
readonly query: (sql: string) => string
}>() {}

v4: Context.Service class syntax

import { Context } from "effect"
class Database extends Context.Service<Database, {
readonly query: (sql: string) => string
}>()("Database") {}

Note the difference in argument order: in v3, the identifier string is passed to Context.Tag(id) before the type parameters. In v4, the type parameters come first via Context.Service<Self, Shape>() and the identifier string is passed to the returned constructor (id).

Effect.Tag Accessors → Context.Service with use

Section titled “Effect.Tag Accessors → Context.Service with use”

v3’s Effect.Tag provided proxy access to service methods as static properties on the tag class (accessors). This allowed calling service methods directly without first yielding the service:

// v3 — static accessor proxy
const program = Notifications.notify("hello")

This pattern had significant limitations. The proxy was implemented via mapped types over the service shape, which meant generic methods lost their type parameters. A service method like get<T>(key: string): Effect<T> would have its generic erased when accessed through the proxy, collapsing to get(key: string): Effect<unknown>. For the same reason, overloaded signatures were not preserved.

In v4, accessors are removed. The most direct replacement is Service.use, which receives the service instance and runs a callback:

v3

import { Effect } from "effect"
class Notifications extends Effect.Tag("Notifications")<Notifications, {
readonly notify: (message: string) => Effect.Effect<void>
}>() {}
// Static proxy access
const program = Notifications.notify("hello")

v4 — use

import { Context, Effect } from "effect"
class Notifications extends Context.Service<Notifications, {
readonly notify: (message: string) => Effect.Effect<void>
}>()("Notifications") {}
// use: access the service and call a method in one step
const program = Notifications.use((n) => n.notify("hello"))

use takes an effectful callback (service: Shape) => Effect<A, E, R> and returns an Effect<A, E, R | Identifier>. useSync takes a pure callback (service: Shape) => A and returns an Effect<A, never, Identifier>. Both return Effects — useSync just allows the accessor function itself to be synchronous:

// ┌─── Effect<void, never, Notifications>
// ▼
const program = Notifications.use((n) => n.notify("hello"))
// ┌─── Effect<number, never, Config>
// ▼
const port = Config.useSync((c) => c.port)

Prefer yield* over use in most cases. While use is a convenient one-liner, it makes it easy to accidentally leak service dependencies into return values. When you call use, the service is available inside the callback but the dependency is not visible at the call site — making it harder to track which services your code depends on. Using yield* in a generator makes dependencies explicit and keeps service access co-located with the rest of your effect logic:

const program = Effect.gen(function*() {
const notifications = yield* Notifications
yield* notifications.notify("hello")
yield* notifications.notify("world")
})

Effect.ServiceContext.Service with make

Section titled “Effect.Service → Context.Service with make”

v3’s Effect.Service allowed defining a service with an effectful constructor and dependencies inline. In v4, use Context.Service with a make option.

v3

In v3, Effect.Service automatically generated a .Default layer from the provided constructor, and wired dependencies into it:

import { Effect, Layer } from "effect"
class Logger extends Effect.Service<Logger>()("Logger", {
effect: Effect.gen(function*() {
const config = yield* Config
return { log: (msg: string) => Effect.log(`[${config.prefix}] ${msg}`) }
}),
dependencies: [Config.Default]
}) {}
// Logger.Default is auto-generated: Layer<Logger, never, never>
// (dependencies are already wired in)
const program = Effect.gen(function*() {
const logger = yield* Logger
yield* logger.log("hello")
}).pipe(Effect.provide(Logger.Default))

v4

In v4, Context.Service with make stores the constructor effect on the class but does not auto-generate a layer. Define layers explicitly using Layer.effect:

import { Context, Effect, Layer } from "effect"
class Logger extends Context.Service<Logger>()("Logger", {
make: Effect.gen(function*() {
const config = yield* Config
return { log: (msg: string) => Effect.log(`[${config.prefix}] ${msg}`) }
})
}) {
// Build the layer yourself from the make effect
static readonly layer = Layer.effect(this, this.make).pipe(
Layer.provide(Config.layer)
)
}

The dependencies option no longer exists. Wire dependencies via Layer.provide as shown above.

Note: v4 adopts the convention of naming layers with layer (e.g. Logger.layer) instead of v3’s Default or Live. Use layer for the primary layer and descriptive suffixes for variants (e.g. layerTest, layerConfig).

v3: Context.Reference

import { Context } from "effect"
class LogLevel extends Context.Reference<LogLevel>()("LogLevel", {
defaultValue: () => "info" as const
}) {}

v4: Context.Reference

import { Context } from "effect"
const LogLevel = Context.Reference<"info" | "warn" | "error">("LogLevel", {
defaultValue: () => "info" as const
})
v3 v4
Context.GenericTag<T>(id) Context.Service<T>(id)
Context.Tag(id)<Self, Shape>() Context.Service<Self, Shape>()(id)
Effect.Tag(id)<Self, Shape>() Context.Service<Self, Shape>()(id)
Effect.Service<Self>()(id, opts) Context.Service<Self>()(id, { make })
Context.Reference<Self>()(id, opts) Context.Reference<T>(id, opts)
Context.make(tag, impl) Context.make(tag, impl)
Context.get(ctx, tag) Context.get(map, tag)
Context.add(ctx, tag, impl) Context.add(map, tag, impl)
Context.mergeAll(...) Context.mergeAll(...)