DI Container usage
The eridu-tech/di component provides an Inversion of Control (IoC) container for managing service registrations, dependency resolution, and object lifetimes.
Initial Configuration
To begin using the DI container, create a Container instance and provide an IExecutionContext:
import { Container } from "eridu-tech/di";
import { AlsExecutionContextAdapter } from "eridu-tech/execution-context/als-execution-context-adapter";
import { ExecutionContext } from "eridu-tech/execution-context";
const executionContext = new ExecutionContext(new AlsExecutionContextAdapter());
export const container = new Container({
executionContext,
});
DI Basics
Overview
The container follows a strict lifecycle.
Register Services and Container Hooks
Register your services by defining their lifespans, dependencies, and service factories. Register hooks that will run after container initialization or de-initialization. All registrations must occur before initialization. The following methods are used to register services: registerFactory, registerValue, registerDynamic, registerProvider. The following are used to register container hooks: onContainerInit and onContainerDeInit.
Services and hooks can only be registered before the container is initialized. Once the container is initialized, registering new services or hooks will throw InvalidMethodCallDiError.
Initialize and Activate the Container
Call init() to prepare the container. The method init() executes all registered initialization hooks.
The current implementation of IContainer is eager. The container will instantiate all services ahead of time rather than lazily upon first resolution. The current implementation also validates the dependency graph when init() is called.
Use the Container
Resolve service instances and run scoped executions. The following methods are used to resolve services: resolve, resolveOr, resolveOrFail. The following method is used to check if a service is resolvable: has. The following method is used to run scoped executions: run.
Services can only be resolved while the container is in an active state (after initialization and before de-initialization). Resolving services before initialization or after de-initialization will throw InvalidMethodCallDiError.
De-Initialize the Container
Call deInit() to tear down the container. The method deInit() executes all registered de-initialization hooks.
Tokens
A token is the key that identifies a service in the container. It is used both to register a service and to resolve it later. A token can be either a class constructor or a generic token created via genericToken().
To create a token using genericToken(), pass a string describing the service and an optional phantom type parameter. The phantom type exists purely for static type checking. It holds no runtime value and is used by TypeScript to infer the correct service type upon resolution.
Generic token
Example of a generic token created with the genericToken method:
import { genericToken } from "eridu-tech/di/contracts";
import type { IDatabase } from "./idatabase.js";
// token created with genericToken where
// `"Database service"` is the description and `IDatabase` is the phantom type.
export const IDATABASE = genericToken<IDatabase>("Database service");
The Database service interface:
export interface IDatabase {
query(sql: string, params: Array<unknown>): Promise<unknown>;
connect(): Promise<void>;
disconnect(): Promise<void>;
}
Class constructor token
Example of a class constructor used as a token:
import { Database } from "./database.js";
// Database's class constructor used as token.
const DATABASE = Database;
The Database class:
import type { IDatabase } from "./idatabase.js";
export class Database implements IDatabase {
query(sql: string, params: Array<unknown>): Promise<unknown> {
/* ... */
return Promise.resolve();
}
async connect(): Promise<void> {
console.log("db connected");
}
async disconnect(): Promise<void> {
console.log("db disconnected");
}
}
Lifetime
When registering a service, you also define its lifetime. There are four different service lifetimes:
-
Singleton — The container creates a single instance of the service for its entire lifetime and shares it across every resolve call and scope.
-
Scoped — The container creates one instance of the service per
run()scope and shares it whenever you resolve the service within that scope. For more details, see the scoped execution section. -
Transient — The container creates a new instance of the service every time you resolve the service and never shares it.
-
Dynamic — The service is declared but has no service factory registered with it. The service factory will be provided dynamically within a
run()scope before it can be resolved. For more details, see the dynamic registration section.
Registration
The container provides four registration methods:
registerFactory— Registers a service using a factory function that creates the instance. Use it to register Singleton, Scoped, or Transient services with full control over how the instance is constructed.registerValue— Registers a pre-constructed value or constant. Values are always resolved as singletons.registerDynamic— Registers a token whose value is not known at registration time and is provided later at runtime, perrun()scope.registerProvider— Registers a service provider that batches a group of related registrations into one reusable code block.
registerFactory
Use registerFactory() to register a Singleton, Scoped, or Transient service using a service factory function. It takes the following arguments:
-
token— The key that identifies the service. -
deps— The dependencies required by the service, defined as a record where each value is a token identifying a dependency. Pass an empty object literal{}if the service has no dependencies. -
factory—invocable(function or object withinvokemethod) that creates and returns the service instance. It receives a record of resolved dependencies as its first argument and theexecution contextas its second argument. The factory can also beasyncand return aPromise. -
lifetime— The lifetime of the service. Must be eitherLIFETIME.SINGLETON,LIFETIME.TRANSIENTorLIFETIME.SCOPED.
Here is a simple example of registerFactory() with no dependencies:
import { LIFETIME } from "eridu-tech/di/contracts";
import { container } from "./container.js";
import { Database } from "./database.js";
import { IDATABASE } from "./generic-token.js";
// `IDATABASE` service requires no dependency
container.registerFactory({
token: IDATABASE,
deps: {}, // No dependencies
factory: (deps) => new Database(),
lifetime: LIFETIME.SINGLETON,
});
The UserProvider service used below depends on the Database service:
import type { IDatabase } from "./idatabase.js";
export interface User {
firstName: string;
lastName: string;
email: string;
id: string;
}
export class UserProvider {
constructor(private database: IDatabase) {
/* ... */
}
getUser(id: string): User {
/* ... */
return {
email: "someone@email.com",
firstName: "some",
lastName: "one",
id: "000001",
};
}
}
Here is a simple example of registerFactory() with one dependency:
import { LIFETIME } from "eridu-tech/di/contracts";
import { container } from "./container.js";
import { IDATABASE } from "./generic-token.js";
import { UserProvider } from "./user-provider.js";
// `UserProvider` service requires `IDATABASE` dependency
container.registerFactory({
token: UserProvider,
deps: { db: IDATABASE },
factory: (deps) => new UserProvider(deps.db),
lifetime: LIFETIME.SINGLETON,
});
The REQUEST_ID token:
import { genericToken } from "eridu-tech/di/contracts";
// A context token for the current request id
export const REQUEST_ID = genericToken<string>("RequestId");
Here is an example of registerFactory() that reads a value from the executionContext:
import { LIFETIME } from "eridu-tech/di/contracts";
import { container } from "./container.js";
import { REQUEST_ID } from "./request-id.js";
class RequestService {
constructor(private requestId: string) {
/* ... */
}
}
container.registerFactory({
token: RequestService,
deps: {},
factory: (deps, executionContext) => {
// Read a contextual value propagated through the resolution chain
const requestId = executionContext.get(REQUEST_ID) ?? "unknown";
return new RequestService(requestId);
},
lifetime: LIFETIME.TRANSIENT,
});
Here is an example of a service factory defined as an object with an invoke method.
import type { ServiceFactory } from "eridu-tech/di/contracts";
const serviceAsObject = {
invoke() {
return "hello";
},
} satisfies ServiceFactory;
// functionally equivalent to serviceAsFunction
const serviceAsFunction = (() => "hello") satisfies ServiceFactory;
registerValue
The CONFIG token:
import { genericToken } from "eridu-tech/di/contracts";
export interface AppConfig {
apiUrl: string;
timeout: number;
}
export const CONFIG = genericToken<AppConfig>("AppConfig");
Use registerValue() to register values as singletons.
import { container } from "./container.js";
import { CONFIG } from "./app-config.js";
container.registerValue({
token: CONFIG,
value: {
apiUrl: "https://api.example.com",
timeout: 5000,
},
});
registerProvider
Use registerProvider() to encapsulate a group of related registrations into a reusable, isolated code block. A service provider can be either:
- A plain function that receives an
IServiceRegisterto register services. - A class with an
invoke(register: IServiceRegister)method.
The Logger services:
export interface ILogger {
log(message: string): void;
}
export class Logger implements ILogger {
log(message: string): void {
/* ... */
}
}
export class ConsoleLogger implements ILogger {
log(message: string): void {
/* ... */
}
}
export class FileLogger implements ILogger {
log(message: string): void {
/* ... */
}
}
import {
LIFETIME,
type IServiceRegister,
type IServiceProvider,
} from "eridu-tech/di/contracts";
import { container } from "./container.js";
import { Database } from "./database.js";
import { FileLogger, Logger } from "./logger.js";
import { UserProvider } from "./user-provider.js";
// As a plain function
function loggingProvider(register: IServiceRegister): void {
register.registerFactory({
token: Logger,
factory: () => new Logger(),
deps: {},
lifetime: LIFETIME.SINGLETON,
});
register.registerFactory({
token: FileLogger,
factory: () => new FileLogger(),
deps: {},
lifetime: LIFETIME.SINGLETON,
});
}
// As a class with an invoke(register: IServiceRegister) method
class DatabaseProvider implements IServiceProvider {
invoke(register: IServiceRegister): void {
register.registerFactory({
token: Database,
factory: () => new Database(),
deps: {},
lifetime: LIFETIME.SINGLETON,
});
register.registerFactory({
token: UserProvider,
factory: ({ db }) => new UserProvider(db),
deps: { db: Database },
lifetime: LIFETIME.SCOPED,
});
}
}
// Register providers
container.registerProvider(loggingProvider);
container.registerProvider(new DatabaseProvider());
Service providers are the recommended way to organize your registrations. Group related services together and keep each provider focused on a single concern.
Registering a Service as Dynamic
Registering a service as dynamic is covered in its own section. For details on how to register and use dynamic services, see the Dynamic Registration section.
Resolving a Service
There are three methods for resolving a service, and one method for checking whether a service can be resolved.
Before resolving any service, the container must be initialized by calling and awaiting init().
resolve
Returns the service if found, null otherwise:
import { container } from "./container.js";
import { Logger } from "./logger.js";
container.registerValue({
token: Logger,
value: new Logger(),
});
await container.init();
const logger = await container.resolve(Logger);
if (logger) {
logger.log("Logger is available");
}
resolveOr
Returns the service if found, otherwise returns the provided default value:
import { container } from "./container.js";
import { ConsoleLogger, Logger } from "./logger.js";
await container.init();
const logger = await container.resolveOr(Logger, new ConsoleLogger());
logger.log("Always has a logger");
resolveOrFail
Returns the service if found, otherwise throws CanNotResolveServiceDiError:
import { CanNotResolveServiceDiError } from "eridu-tech/di/contracts";
import { container } from "./container.js";
import { Logger } from "./logger.js";
await container.init();
// Throws CanNotResolveServiceDiError if Logger is not registered
try {
await container.resolveOrFail(Logger);
} catch (error) {
if (error instanceof CanNotResolveServiceDiError) {
console.error(error);
} else {
throw error;
}
}
has
Returns true if the token can be resolved, or false otherwise.
import { container } from "./container.js";
import { Logger } from "./logger.js";
container.registerValue({
token: Logger,
value: new Logger(),
});
await container.init();
if (await container.has(Logger)) {
console.log("Logger is resolvable");
}
The method has() checks whether a service can be resolved, not whether it is registered.
Calling has() may invoke service factories as a side effect.
Scoped
The run() method creates an isolated scope where scoped services are resolved once and then discarded.
import { LIFETIME } from "eridu-tech/di/contracts";
import { container } from "./container.js";
class A {
// ...
}
// Register a scoped service
container.registerFactory({
token: A,
deps: {},
factory: () => new A(),
lifetime: LIFETIME.SCOPED,
});
await container.init();
await container.run({
scope: async () => {
// Scoped services are resolved once within this scope
const a1 = await container.resolveOrFail(A);
const a2 = await container.resolveOrFail(A);
console.log(a1 === a2); // true
// A nested scope creates a new scoped registry, so it gets its own
// instance of the scoped service
await container.run({
scope: async () => {
const nestedA = await container.resolveOrFail(A);
console.log(nestedA === a1); // false
},
});
},
});
// Outside the scope, scoped services are no longer available
// A new scope would create new instances
Before calling run(), the container must be initialized by calling and awaiting init().
Dynamic
Use registerDynamic() when a token's value is not known at registration time and must be provided later at runtime — for example, values derived from an incoming request:
import { container } from "./container.js";
import { REQUEST_ID } from "./request-id.js";
// Register as dynamic — value will be provided later
container.registerDynamic(REQUEST_ID);
Dynamic values are set at runtime using the IDynamicServiceRegister interface, inside a scoped run() execution.
import { LIFETIME } from "eridu-tech/di/contracts";
import { container } from "./container.js";
import { REQUEST_ID } from "./request-id.js";
import { RequestHandler } from "./request-handler.js";
// Declare the token as dynamic — its value is only known per request
container.registerDynamic(REQUEST_ID);
// A scoped service can use the dynamic value as a dependency
container.registerFactory({
token: RequestHandler,
deps: { requestId: REQUEST_ID },
factory: ({ requestId }) => new RequestHandler(requestId),
lifetime: LIFETIME.SCOPED,
});
await container.init();
await container.run({
registration: async (register) => {
// Set the dynamic value before the scope executes
register.set({
token: REQUEST_ID,
value: crypto.randomUUID(),
});
},
scope: async () => {
// The scoped service is injected with the per-request dynamic value
const handler = await container.resolveOrFail(RequestHandler);
await handler.handle();
},
});
The RequestHandler:
export class RequestHandler {
constructor(private requestId: string) {
/* ... */
}
async handle(): Promise<void> {
console.log(`Handling request: ${this.requestId}`);
}
}
IDynamicServiceRegister exposes get(), getOrFail() and has() to retrieve values from the execution context, alongside set which stores a value in it.
For example, CORRELATION_ID is another dynamic token (registered with registerDynamic()) whose value may already be present in the execution context:
import { LIFETIME } from "eridu-tech/di/contracts";
import { container } from "./container.js";
import { REQUEST_ID } from "./request-id.js";
import { RequestHandler } from "./request-handler.js";
// Declare the token as dynamic — its value is only known per request
container.registerDynamic(REQUEST_ID);
// A scoped service can use the dynamic value as a dependency
container.registerFactory({
token: RequestHandler,
deps: { requestId: REQUEST_ID },
factory: ({ requestId }) => new RequestHandler(requestId),
lifetime: LIFETIME.SCOPED,
});
await container.init();
await container.run({
registration: async (register) => {
// Set the dynamic value before the scope executes
register.set({
token: REQUEST_ID,
value: crypto.randomUUID(),
});
},
scope: async () => {
// The scoped service is injected with the per-request dynamic value
const handler = await container.resolveOrFail(RequestHandler);
await handler.handle();
},
});
IDynamicServiceRegister also provide following methods: getOrFail() throws CanNotResolveServiceDiError when no value is available, and has() lets you check for a value without reading it.
The methods get(), has() and getOrFail() only consider a token as existing when it is registered as dynamic and has a value in the execution context. if the token is not registered as dynamic, or it is registered as dynamic but has no value in the execution context it will not considered as existing.
set() writes the value directly to the execution context. If the token already has a value in the execution context, that value is implicitly overwritten. If the token does not exist in the execution context yet, the value is stored with the token as key.
Dynamic values are saved to and retrieved from the execution context. set() stores the value in the execution context, while get(), has() and getOrFail read it from there.
Lifetime Relationship
The container validates the lifetime relationships between a service and its declared dependencies, enforcing the rules described below. Any relationship not listed above throws InvalidGraphDiError.
| Service lifetime | Can depend on service lifetimes |
|---|---|
| Singleton | Singleton |
| Scoped | Singleton, Scoped or Dynamic |
| Transient | Singleton, Scoped |
| Dynamic | None — Dynamic can not depend on others |
The current implementation of IContainer will validate the dependency graph at init() or when a service is overridden with overrideFactory().
If any invalid relationship is found, InvalidGraphDiError will be thrown.
Only a Scoped service can depend on a Dynamic service. A Transient service cannot depend directly on a Dynamic service. A Dynamic service cannot depend on others, even on other Dynamic services.
Example of a valid relationship — a transient service depending on a singleton service:
import { LIFETIME } from "eridu-tech/di/contracts";
import { container } from "./container.js";
import { Database } from "./database.js";
import { UserProvider } from "./user-provider.js";
container.registerFactory({
token: Database,
factory: () => new Database(),
deps: {},
lifetime: LIFETIME.SINGLETON,
});
// ✅ Service is registered as `LIFETIME.TRANSIENT`
// and its `db` dependency is `LIFETIME.SINGLETON`
container.registerFactory({
token: UserProvider,
factory: ({ db }) => new UserProvider(db),
deps: { db: Database },
lifetime: LIFETIME.TRANSIENT,
});
await container.init(); // will not throw InvalidGraphDiError
The dependency chain used below:
// Dependency chain used to demonstrate lifetime relationships.
// The chain is: C → B → A
export class A {}
export class B {
constructor(private a: A) {
/* ... */
}
}
export class C {
constructor(private b: B) {
/* ... */
}
}
Example of an invalid relationship — a singleton service depending on a transient service:
import { InvalidGraphDiError, LIFETIME } from "eridu-tech/di/contracts";
import { container } from "./container.js";
import { A, B, C } from "./dependency-chain.js";
container.registerFactory({
token: A,
factory: () => new A(),
deps: {},
lifetime: LIFETIME.TRANSIENT,
});
container.registerFactory({
token: B,
factory: ({ a }) => new B(a),
deps: { a: A },
lifetime: LIFETIME.TRANSIENT,
});
// ❌ Service is registered as `LIFETIME.SINGLETON`
// and its `transient` dependency is `LIFETIME.TRANSIENT`
container.registerFactory({
token: C,
factory: ({ b }) => new C(b),
deps: { b: B },
lifetime: LIFETIME.SINGLETON,
});
// will throw InvalidGraphDiError
try {
await container.init();
} catch (error) {
if (error instanceof InvalidGraphDiError) {
console.error(error);
} else {
throw error;
}
}
Container Hooks
You can register multiple initialization hooks by calling onContainerInit() multiple times, and multiple de-initialization hooks by calling onContainerDeInit() multiple times. Initialization hooks run when container.init() is called, while de-initialization hooks run when container.deInit() is called.
Both callbacks for onContainerInit() and onContainerDeInit() receive an object that can be used to resolve services with resolve, resolveOr, resolveOrFail and check resolvability with has.
Hooks must be registered before container.init() is called. Calling onContainerInit() or onContainerDeInit() after container.init() throws InvalidMethodCallDiError.
import { LIFETIME } from "eridu-tech/di/contracts";
import { container } from "./container.js";
import { Database } from "./database.js";
container.registerFactory({
token: Database,
factory: () => new Database(),
deps: {},
lifetime: LIFETIME.SINGLETON,
});
container.onContainerInit(async (resolver) => {
// Runs when container.init() is called
// Use the resolver to resolve services after all registrations are complete
const db = await resolver.resolveOrFail(Database);
await db.connect();
console.log("Container initialized");
});
container.onContainerDeInit(async (resolver) => {
// Runs when container.deInit() is called
const db = await resolver.resolveOrFail(Database);
await db.disconnect();
console.log("Container deinitialized");
});
// Trigger the lifecycle
await container.init();
// ... application runs ...
await container.deInit();
Overriding Registrations
To override a registered service factory, use overrideFactory(); to override a registered singleton value, use overrideValue(). A service can only be overridden once. If the token is not registered, is registered as dynamic, or has already been overridden, a CanNotOverrideServiceDiError is thrown.
We recommend using overrides only during testing, not in production code. Overriding is useful for mocking services or swapping implementations. For example, replacing a real database with an in-memory adapter.
Overriding a registration is forbidden after the container is initialized. Calling overrideFactory() or overrideValue() after container.init() throws InvalidMethodCallDiError.
import { LIFETIME } from "eridu-tech/di/contracts";
import { container } from "./container.js";
import { CONFIG } from "./app-config.js";
import { Database } from "./database.js";
import type { IDatabase } from "./idatabase.js";
import { IDATABASE } from "./generic-token.js";
container.registerFactory({
token: IDATABASE,
factory: () => new Database(),
deps: {},
lifetime: LIFETIME.SINGLETON,
});
container.registerValue({
token: CONFIG,
value: { apiUrl: "https://api.example.com", timeout: 5000 },
});
class MockDatabase implements IDatabase {
query(sql: string, params: Array<unknown>): Promise<unknown> {
/* ... */
return Promise.resolve();
}
async connect(): Promise<void> {
console.log("connected");
}
async disconnect(): Promise<void> {
console.log("disconnected");
}
}
// Override a registered factory service
container.overrideFactory({
token: IDATABASE,
factory: async (_deps, _executionContext) => {
// Return a mock database for testing
return new MockDatabase();
},
deps: {},
});
// Override a registered singleton value
container.overrideValue({
token: CONFIG,
value: { apiUrl: "http://localhost:9999", timeout: 100 },
});
Forking a Container
The fork() method creates a child container that inherits all registrations and overrides from the parent at the moment of forking. After that, the two containers are fully isolated: registering or overriding services in the child does not affect the parent, and registering or overriding services in the parent does not affect the child.
We recommend using forking only during testing. It is useful for testing different adapters by having one common base container and a fork for each adapter.
Forking is forbidden after the container is initialized. Calling fork() after container.init() throws InvalidMethodCallDiError.
import { container } from "./container.js";
import { CONFIG } from "./app-config.js";
container.registerValue({
token: CONFIG,
value: { apiUrl: "https://api.example.com", timeout: 5000 },
});
const childContainer = container.fork();
// Override in the child container — parent is unaffected
childContainer.overrideValue({
token: CONFIG,
value: { apiUrl: "http://test.local", timeout: 100 },
});
// Both containers must be initialized before resolving
await container.init();
await childContainer.init();
// Original container still has the original config
const parentConfig = await container.resolveOrFail(CONFIG);
const childConfig = await childContainer.resolveOrFail(CONFIG);
console.log(parentConfig.apiUrl); // "https://api.example.com"
console.log(childConfig.apiUrl); // "http://test.local"
Errors
Most errors expose an error flag via the flag class field, along with detailed context via the info class field.
| Error | Description |
|---|---|
CanNotResolveServiceDiError | Thrown when a service cannot be resolved. |
InvalidGraphDiError | Thrown when the service graph is invalid. |
CanNotRegisterServiceDiError | Thrown when a service cannot be registered. |
CanNotOverrideServiceDiError | Thrown when a registration cannot be overridden. |
InvalidMethodCallDiError | Thrown when a container method is called at an invalid time or context. |
CanNotRegisterServiceDiError
Thrown when a service cannot be registered. It has the following flags:
| Flag | Description |
|---|---|
ALREADY_REGISTERED | Thrown when the token already has a registration. |
DYNAMIC_SERVICE_PROVIDER_REGISTRATION_TOKEN_IS_NOT_DYNAMIC | Thrown when the token provided to a dynamic service provider is not a dynamic token. |
DYNAMIC_SERVICE_PROVIDER_REGISTRATION_TOKEN_DO_NOT_EXIST | Thrown when the token provided to a dynamic service provider does not exist. |
Here is an example where CanNotRegisterServiceDiError is thrown.
import { CanNotRegisterServiceDiError } from "eridu-tech/di/contracts";
import { container } from "./container.js";
import { CONFIG } from "./app-config.js";
container.registerValue({
token: CONFIG,
value: { apiUrl: "https://api.example.com", timeout: 5000 },
});
// Throws CanNotRegisterServiceDiError because CONFIG token is already registered
try {
container.registerValue({
token: CONFIG,
value: { apiUrl: "https://another.example.com", timeout: 3000 },
});
} catch (error) {
if (error instanceof CanNotRegisterServiceDiError) {
console.error(error);
} else {
throw error;
}
}
InvalidGraphDiError
Thrown when the service graph is invalid. It has the following flags:
| Flag | Description |
|---|---|
INVALID_EDGE_RELATIONSHIP | Thrown when a service depends on another service with an incompatible lifetime. |
CYCLE_DEPENDENCY | Thrown when there is a dependency cycle among services. |
UNDECLARED_DEPENDENCIES | Thrown when a declared dependency is not registered. |
Here is an example where InvalidGraphDiError is thrown.
import { InvalidGraphDiError, LIFETIME } from "eridu-tech/di/contracts";
import { container } from "./container.js";
import { A, B, C } from "./dependency-chain.js";
container.registerFactory({
token: A,
factory: () => new A(),
deps: {},
lifetime: LIFETIME.TRANSIENT,
});
container.registerFactory({
token: B,
factory: ({ a }) => new B(a),
deps: { a: A },
lifetime: LIFETIME.TRANSIENT,
});
// ❌ A singleton (C) cannot depend on a transient (B)
container.registerFactory({
token: C,
factory: ({ b }) => new C(b),
deps: { b: B },
lifetime: LIFETIME.SINGLETON,
});
// Throws InvalidGraphDiError because a singleton depends on a transient service
try {
await container.init();
} catch (error) {
if (error instanceof InvalidGraphDiError) {
console.error(error);
} else {
throw error;
}
}
CanNotResolveServiceDiError
Thrown when a service cannot be resolved. It has the following flags:
| Flag | Description |
|---|---|
NOT_REGISTERED_TOKEN | Thrown when the token is not registered. |
SCOPED_SERVICE_OUTSIDE_RUN | Thrown when a scoped service is resolved outside a run() scope. |
DYNAMIC_SERVICE_OUTSIDE_RUN | Thrown when a dynamic service is resolved outside a run() scope. |
TRANSIENT_SERVICE_DEPEND_ON_SCOPED_WHO_CALLED_OUTSIDE_RUN | Thrown when a transient service depends on a scoped service and is resolved outside a run() scope. |
RESOLVED_VALUE_IS_NULL | Thrown when the resolved value is null. |
NO_DYNAMIC_VALUE_SET_FOR_TOKENS | Thrown when a dynamic token has no value set. |
DYNAMIC_SERVICE_PROVIDER_NOT_DYNAMIC_TOKEN | Thrown when the token provided to a dynamic service provider is not a dynamic token. |
import { CanNotResolveServiceDiError } from "eridu-tech/di/contracts";
import { container } from "./container.js";
import { Logger } from "./logger.js";
await container.init();
// Throws CanNotResolveServiceDiError because Logger is not registered
try {
await container.resolveOrFail(Logger);
} catch (error) {
if (error instanceof CanNotResolveServiceDiError) {
console.error(error);
} else {
throw error;
}
}
CanNotOverrideServiceDiError
Thrown when a registration cannot be overridden. It has the following flags:
| Flag | Description |
|---|---|
TOKEN_NOT_REGISTERED | Thrown when the token is not registered. |
DYNAMIC_TOKEN | Thrown when the token is registered as dynamic and cannot be overridden. |
ALREADY_OVERRIDDEN | Thrown when the service has already been overridden. |
Here is an example where CanNotOverrideServiceDiError is thrown.
import { CanNotOverrideServiceDiError } from "eridu-tech/di/contracts";
import { container } from "./container.js";
import { CONFIG } from "./app-config.js";
// Throws CanNotOverrideServiceDiError because CONFIG token is not registered
// and hence cannot be overridden
try {
container.overrideValue({
token: CONFIG,
value: { apiUrl: "http://localhost:9999", timeout: 100 },
});
} catch (error) {
if (error instanceof CanNotOverrideServiceDiError) {
console.error(error);
} else {
throw error;
}
}
InvalidMethodCallDiError
Thrown when a container method is called at an invalid time or context. It has the following flags:
| Flag | Description |
|---|---|
NOT_ACTIVE | Thrown when a method is called while the container is not active (not initialized). |
ALREADY_INITIALIZED | Thrown when a registration method is called after the container was initialized. |
INSIDE_RUN | Thrown when a method is called inside a run() scope where it is not allowed. |
INSIDE_DYNAMIC_REGISTRATION | Thrown when a method is called inside the dynamic registration callback. |
OUTSIDE_RUN | Thrown when a method is called outside a run() scope where a scope is required. |
Here is an example where InvalidMethodCallDiError is thrown.
import { InvalidMethodCallDiError } from "eridu-tech/di/contracts";
import { container } from "./container.js";
import { CONFIG } from "./app-config.js";
await container.init();
// Throws InvalidMethodCallDiError because registration is attempted after init()
try {
container.registerValue({
token: CONFIG,
value: { apiUrl: "https://another.example.com", timeout: 3000 },
});
} catch (error) {
if (error instanceof InvalidMethodCallDiError) {
console.error(error);
} else {
throw error;
}
}
Patterns
Separating Registration and Resolution Concerns
The container exposes several contracts that separate concerns for different use cases:
IServiceRegister— for registering services (registerFactory,registerValue,registerDynamic,registerProvider) and registering container lifecycle hooks.IServiceResolver— for resolving services (resolve,resolveOr,resolveOrFail,has).IServiceOverrider— for overriding existing registrations (overrideFactory,overrideValue), useful for testing.IContainerScope— for running scoped container executions (run).IContainerFork— for forking a child container (fork), useful for testing.IDynamicServiceRegister— for setting dynamic values at runtime (set).
IServiceRegister
registerFactory(settings)registerValue(settings)registerDynamic(token)registerProvider(provider)onContainerInit(handler)onContainerDeInit(handler)
IServiceResolver
IServiceOverrider
IContainerScope
IContainerFork
Further information
For further information refer to eridu-tech/di API docs.