The Context module in Effect is the foundation for dependency injection and type-safe service composition. It provides a way to:
- Define services as typed "tags" (unique identifiers)
- Build a container (
Context<R>) holding service implementations - Track dependencies at the type level via the
R(Requirements) type parameter - Provide implementations to effects at runtime
Effect<Success, Error, Requirements>;
// ^ ^ ^
// | | └── Contextual dependencies (services needed)
// | └────────── Error type (can fail with)
// └─────────────────── Success type (succeeds with)The Requirements parameter (R) tracks which services an Effect needs. When R = never, the effect has no dependencies and can run immediately.
Context.Tag creates a unique identifier for a service. It's a type-level marker that connects:
- A service identifier (the tag itself)
- A service interface (what the service provides)
import { Context, Effect } from "effect";
// Define a service tag
class Random extends Context.Tag("MyRandomService")<
Random, // Tag identifier type
{ readonly next: Effect.Effect<number> } // Service interface
>() {}Key insight: The string "MyRandomService" is a runtime identifier for debugging. The type system uses the class itself (Random) as the unique key.
Once defined, yield the tag in Effect.gen to access the service:
const program = Effect.gen(function* () {
const random = yield* Random; // Accesses the service
const value = yield* random.next; // Uses service methods
return value;
});
// Type: Effect<number, never, Random>
// ^^^^^
// Requires Random serviceUse Effect.provideService to supply an implementation:
const runnable = Effect.provideService(program, Random, {
next: Effect.sync(() => Math.random()),
});
// Type: Effect<number, never, never>
// ^^^^^
// No more requirements!Direct tag definition. Requires manual Layer creation.
class Database extends Context.Tag("Database")<
Database,
{ readonly query: (sql: string) => Effect.Effect<Array<unknown>> }
>() {}
// Create layer manually
const DatabaseLive = Layer.succeed(Database, {
query: (sql) => Effect.succeed([]),
});Use when:
- Simple services without complex initialization
- Middleware patterns (providing services dynamically)
- Maximum control over layer composition
All-in-one service definition with built-in layer and optional accessors.
class Logger extends Effect.Service<Logger>()("Logger", {
// Generates accessors (Logger.info instead of logger.info)
accessors: true,
// Service implementation (effectful)
effect: Effect.gen(function* () {
return {
info: (msg: string) => Effect.log(msg),
};
}),
// Dependencies this service needs
dependencies: [OtherService.Default],
}) {}Use when:
- Services with dependencies on other services
- Services needing lifecycle management (scoped)
- You want auto-generated
Defaultlayer and accessors
| Feature | Context.Tag | Effect.Service |
|---|---|---|
| Boilerplate | Minimal | Slightly more |
| Auto Layer | No | Yes (.Default) |
| Accessors | No | Optional |
| Dependencies | Manual | Declarative |
| Scoped lifecycle | Manual | Built-in (scoped:) |
Effect.Service supports three construction modes:
// 1. Sync constructor - simple, immediate value
class Config extends Effect.Service<Config>()("Config", {
sync: () => ({ apiUrl: "https://api.example.com" }),
}) {}
// 2. Effect constructor - async/effectful initialization
class Database extends Effect.Service<Database>()("Database", {
effect: Effect.gen(function* () {
const config = yield* Config;
return { query: (sql: string) => Effect.succeed([]) };
}),
dependencies: [Config.Default],
}) {}
// 3. Scoped constructor - lifecycle with cleanup
class Connection extends Effect.Service<Connection>()("Connection", {
scoped: Effect.gen(function* () {
const conn = yield* Effect.acquireRelease(
Effect.sync(() => createConnection()),
(conn) => Effect.sync(() => conn.close())
);
return { conn };
}),
}) {}Layers are blueprints for building Context values. They compose services together.
// From simple value
const ConfigLive = Layer.succeed(Config, { logLevel: "INFO" });
// From effectful computation
const DatabaseLive = Layer.effect(
Database,
Effect.gen(function* () {
const config = yield* Config;
return { query: () => Effect.succeed([]) };
})
);
// From scoped resource
const ConnectionLive = Layer.scoped(
Connection,
Effect.gen(function* () {
yield* Effect.addFinalizer(() => Effect.log("Cleanup"));
return {
/* ... */
};
})
);// Vertical: A provides to B
const AppLayer = DatabaseLive.pipe(
Layer.provide(ConfigLive) // Config feeds into Database
);
// Horizontal: Merge independent layers
const CombinedLayer = Layer.merge(LoggerLive, MetricsLive);
// Full application layer
const MainLayer = Layer.mergeAll(DatabaseLive, LoggerLive, CacheLive).pipe(
Layer.provide(ConfigLive) // Shared dependency
);Effect's type system ensures you can't run an effect without providing all dependencies:
const program: Effect<User, DbError, Database | Logger> = Effect.gen(
function* () {
const db = yield* Database;
const logger = yield* Logger;
// ...
}
);
// Providing Database removes it from requirements
const partial = Effect.provide(program, DatabaseLive);
// Type: Effect<User, DbError, Logger>
// Must provide Logger too
const runnable = Effect.provide(partial, LoggerLive);
// Type: Effect<User, DbError, never>
// Now it can run!
Effect.runPromise(runnable);Compiler enforces: No missing dependencies at runtime.
The Context module provides low-level operations (rarely used directly):
| Function | Purpose |
|---|---|
Context.empty() |
Create empty context |
Context.make(tag, value) |
Create context with single service |
Context.add(context, tag, value) |
Add service to context |
Context.get(context, tag) |
Get service (Option) |
Context.unsafeGet(context, tag) |
Get service (throws if missing) |
Context.merge(ctx1, ctx2) |
Combine two contexts |
Most code uses Layer and Effect.provide* instead of raw Context operations.
class Counter extends Context.Tag("Counter")<Counter, Ref.Ref<number>>() {
static Live = Layer.scoped(this, Ref.make(0));
}class SendTransaction extends Effect.Service<SendTransaction>()(
"SendTransaction",
{
effect: Effect.gen(function* () {
const rdt = yield* RadixDappToolkit;
// Return a function, not a value
return Effect.fn(function* (manifest: string) {
const toolkit = yield* Ref.get(rdt);
return yield* toolkit.send(manifest);
});
}),
}
) {}class RadixDappToolkit extends Context.Tag("RadixDappToolkit")<
RadixDappToolkit,
Ref.Ref<RadixDappToolkitFactory>
>() {
static Live = Layer.scoped(
this,
Effect.gen(function* () {
const rdt = RadixDappToolkitFactory({
/* config */
});
// Register cleanup
yield* Effect.addFinalizer(() => Effect.sync(() => rdt.destroy()));
return yield* Ref.make(rdt);
})
);
}// Wrong - tag not yielded
const program = Effect.gen(function* () {
const value = Database.query("SELECT 1"); // Oops!
});
// Correct
const program = Effect.gen(function* () {
const db = yield* Database;
const value = yield* db.query("SELECT 1");
});// If B depends on A, provide A first (or use Layer.provide)
const wrong = Layer.merge(BLive, ALive); // B can't find A
const correct = BLive.pipe(Layer.provide(ALive));// Wrong - Layer.succeed can't handle async
const DbLive = Layer.succeed(Database, await connectToDb());
// Correct - use Layer.effect
const DbLive = Layer.effect(
Database,
Effect.promise(() => connectToDb())
);| Concept | Purpose |
|---|---|
Context.Tag |
Define service identifier |
Effect.Service |
Define service with built-in layer |
Layer |
Blueprint for providing services |
Effect.provide |
Supply dependencies to effect |
R type parameter |
Track dependencies at compile time |
The Context system enables:
- Type-safe DI - Compiler catches missing dependencies
- Testability - Swap implementations easily
- Composability - Build complex apps from simple services
- Resource safety - Scoped services with guaranteed cleanup