Skip to content
Buy me a coffee

Lifecycle and disposal

The container creates your services, so it also cleans them up. Two optional hooks run at the edges of an instance’s life: onInit after it is constructed, and onDestroy (or a standard dispose method) when its owner is disposed.

class
class Database
Database
implements
(alias) interface LifecycleHooks
import LifecycleHooks

Optional lifecycle hooks. onInit runs after construction (class registrations); onDestroy runs when the owning provider or scope is disposed. [Symbol.asyncDispose] and [Symbol.dispose] are honored too.

LifecycleHooks
{
private
Database.pool?: {
end(): Promise<void>;
} | undefined
pool
?: {
function end(): Promise<void>
end
():
interface Promise<T>

Represents the completion of an asynchronous operation

Promise
<void> };
async
Database.onInit(): Promise<void>
onInit
() {
this.
Database.pool?: {
end(): Promise<void>;
} | undefined
pool
= await
function connect(url: string): Promise<{
end(): Promise<void>;
}>
connect
('postgres://localhost/app'); // awaited before anyone receives the instance
}
async
Database.onDestroy(): Promise<void>
onDestroy
() {
await this.
Database.pool?: {
end(): Promise<void>;
} | undefined
pool
?.
function end(): Promise<void>
end
();
}
}
const
const provider: ServiceProvider
provider
= new
new ServiceCollection(): ServiceCollection

Collects service registrations, then builds a

ServiceProvider

. Modeled on .NET's IServiceCollection.

@example

const services = new ServiceCollection();
services.addSingleton(Logger, ConsoleLogger);
services.addScoped(UserService, [Logger]);
const provider = services.buildServiceProvider({ validateScopes: true });

ServiceCollection
().
ServiceCollection.addSingleton<Database>(token: Newable<Database, []>): ServiceCollection (+4 overloads)

Registers a singleton: one instance for the root provider and all its scopes.

  • addSingleton(Class) / addSingleton(Class, [deps]): the class is its own token.
  • addSingleton(token, Implementation, [deps]): deps are checked against the constructor's parameters.
  • addSingleton(token, (provider) => instance): a factory, may be async.

addSingleton
(
class Database
Database
).
ServiceCollection.buildServiceProvider(options?: BuildOptions): ServiceProvider

Builds the provider. Registrations are copied, so changing this collection afterwards does not affect providers already built.

buildServiceProvider
();
const
const db: Database
db
= await
const provider: ServiceProvider
provider
.
ServiceProvider.getRequiredService<Database>(token: Token<Database>): Promise<Database>

Resolves token, throwing ServiceNotFoundError if it is not registered.

getRequiredService
(
class Database
Database
); // connected
await
const provider: ServiceProvider
provider
.
ServiceProvider.dispose(): Promise<void>

Disposes every instance this provider (or scope) created, in reverse creation order: onDestroy(), else [Symbol.asyncDispose](), else [Symbol.dispose](). Disposing the root disposes singletons; disposing a scope disposes its scoped and transient instances. Every instance is disposed even if some throw; the errors are rethrown afterwards (an AggregateError if more than one). Later calls wait for the first one and do not rethrow.

dispose
(); // pool closed

LifecycleHooks is an optional interface for autocompletion. The container only checks whether the methods exist.

  • Runs right after the constructor, and is awaited if it returns a promise. The caller of getRequiredService, and any service that depends on this one, gets the instance only after onInit has finished.
  • Runs once per instance: once for a singleton, once per scope for a scoped service, every time for a transient.
  • Runs for class registrations only. A factory already controls construction, so it does its own async setup before returning; onInit on a factory’s result is not called. Values are never initialised either.
  • If it throws, the resolution rejects with that error and nothing is cached, so the next resolution builds a new instance. The failed instance was already constructed, so it is still disposed with its owner: onDestroy must cope with a half-initialised object.

Use it for async setup a constructor can’t do: opening a connection, warming a cache, reading a file.

await provider.dispose() (or await scope.dispose(), or leaving an await using block) disposes every instance that provider created and still tracks. For each instance it calls the first of these that exists:

  1. onDestroy()
  2. [Symbol.asyncDispose]()
  3. [Symbol.dispose]()

So classes written for the standard explicit resource management protocol work without changes, and a class that has both an onDestroy and a Symbol.dispose is disposed only once.

class
class TempDir
TempDir
{
async [
var Symbol: SymbolConstructor
Symbol
.
SymbolConstructor.asyncDispose: typeof Symbol.asyncDispose

A method that is used to asynchronously release resources held by an object. Called by the semantics of the await using statement.

asyncDispose
]() {
var console: Console

The console module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers.

The module exports two specific components:

  • A Console class with methods such as console.log(), console.error() and console.warn() that can be used to write to any Node.js stream.
  • A global console instance configured to write to process.stdout and process.stderr. The global console can be used without importing the node:console module.

Warning: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the note on process I/O for more information.

Example using the global console:

console.log('hello world');
// Prints: hello world, to stdout
console.log('hello %s', 'world');
// Prints: hello world, to stdout
console.error(new Error('Whoops, something bad happened'));
// Prints error message and stack trace to stderr:
// Error: Whoops, something bad happened
// at [eval]:5:15
// at Script.runInThisContext (node:vm:132:18)
// at Object.runInThisContext (node:vm:309:38)
// at node:internal/process/execution:77:19
// at [eval]-wrapper:6:22
// at evalScript (node:internal/process/execution:76:60)
// at node:internal/main/eval_string:23:3
const name = 'Will Robinson';
console.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to stderr

Example using the Console class:

const out = getStreamSomehow();
const err = getStreamSomehow();
const myConsole = new console.Console(out, err);
myConsole.log('hello world');
// Prints: hello world, to out
myConsole.log('hello %s', 'world');
// Prints: hello world, to out
myConsole.error(new Error('Whoops, something bad happened'));
// Prints: [Error: Whoops, something bad happened], to err
const name = 'Will Robinson';
myConsole.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to err

@see ― source

console
.
Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)

Prints to stdout with newline. Multiple arguments can be passed, with the first used as the primary message and all additional used as substitution values similar to printf(3) (the arguments are all passed to util.format()).

const count = 5;
console.log('count: %d', count);
// Prints: count: 5, to stdout
console.log('count:', count);
// Prints: count: 5, to stdout

See util.format() for more information.

@since ― v0.1.100

log
('temp dir removed');
}
}
const
const provider: ServiceProvider
provider
= new
new ServiceCollection(): ServiceCollection

Collects service registrations, then builds a

ServiceProvider

. Modeled on .NET's IServiceCollection.

@example

const services = new ServiceCollection();
services.addSingleton(Logger, ConsoleLogger);
services.addScoped(UserService, [Logger]);
const provider = services.buildServiceProvider({ validateScopes: true });

ServiceCollection
().
ServiceCollection.addScoped<TempDir>(token: Newable<TempDir, []>): ServiceCollection (+4 overloads)

Registers a scoped service: one instance per scope. Same forms as

addSingleton

.

addScoped
(
class TempDir
TempDir
).
ServiceCollection.buildServiceProvider(options?: BuildOptions): ServiceProvider

Builds the provider. Registrations are copied, so changing this collection afterwards does not affect providers already built.

buildServiceProvider
();
{
await using
await using scope: ServiceProvider
scope
=
const provider: ServiceProvider
provider
.
ServiceProvider.createScope(): ServiceProvider

Creates a scope: scoped services resolve once per scope. Dispose it when done (or use await using).

createScope
();
await
await using scope: ServiceProvider
scope
.
ServiceProvider.getRequiredService<TempDir>(token: Token<TempDir>): Promise<TempDir>

Resolves token, throwing ServiceNotFoundError if it is not registered.

getRequiredService
(
class TempDir
TempDir
);
} // "temp dir removed"

Instances returned by factories are disposed too, if they have one of these methods.

If a provider or scope is disposed while one of its services is still being built (an async factory or onInit is still running), that service is disposed as soon as construction finishes, and the pending get* call rejects with DisposedError. A shutdown in the middle of a request doesn’t leak a connection that was being opened.

Instances are disposed one at a time, awaiting each, in reverse creation order. A service is created after its dependencies, so it is disposed before them: a repository closes before the pool it uses, the pool before the logger.

const
const log: string[]
log
: string[] = [];
class
class Logger
Logger
{
Logger.onDestroy(): void
onDestroy
() {
const log: string[]
log
.
Array<string>.push(...items: string[]): number

Appends new elements to the end of an array, and returns the new length of the array.

@param ― items New elements to add to the array.

push
('Logger');
}
}
class
class Pool
Pool
{
constructor(readonly
Pool.logger: Logger
logger
:
class Logger
Logger
) {}
Pool.onDestroy(): void
onDestroy
() {
const log: string[]
log
.
Array<string>.push(...items: string[]): number

Appends new elements to the end of an array, and returns the new length of the array.

@param ― items New elements to add to the array.

push
('Pool');
}
}
class
class Repo
Repo
{
constructor(readonly
Repo.pool: Pool
pool
:
class Pool
Pool
) {}
Repo.onDestroy(): void
onDestroy
() {
const log: string[]
log
.
Array<string>.push(...items: string[]): number

Appends new elements to the end of an array, and returns the new length of the array.

@param ― items New elements to add to the array.

push
('Repo');
}
}
const
const provider: ServiceProvider
provider
= new
new ServiceCollection(): ServiceCollection

Collects service registrations, then builds a

ServiceProvider

. Modeled on .NET's IServiceCollection.

@example

const services = new ServiceCollection();
services.addSingleton(Logger, ConsoleLogger);
services.addScoped(UserService, [Logger]);
const provider = services.buildServiceProvider({ validateScopes: true });

ServiceCollection
()
.
ServiceCollection.addSingleton<Logger>(token: Newable<Logger, []>): ServiceCollection (+4 overloads)

Registers a singleton: one instance for the root provider and all its scopes.

  • addSingleton(Class) / addSingleton(Class, [deps]): the class is its own token.
  • addSingleton(token, Implementation, [deps]): deps are checked against the constructor's parameters.
  • addSingleton(token, (provider) => instance): a factory, may be async.

addSingleton
(
class Logger
Logger
)
.
ServiceCollection.addSingleton<Pool, [logger: Logger]>(token: Newable<Pool, [logger: Logger]>, dependencies: readonly [logger: Token<Logger>]): ServiceCollection (+4 overloads)

Registers a singleton: one instance for the root provider and all its scopes.

  • addSingleton(Class) / addSingleton(Class, [deps]): the class is its own token.
  • addSingleton(token, Implementation, [deps]): deps are checked against the constructor's parameters.
  • addSingleton(token, (provider) => instance): a factory, may be async.

addSingleton
(
class Pool
Pool
, [
class Logger
Logger
])
.
ServiceCollection.addSingleton<Repo, [pool: Pool]>(token: Newable<Repo, [pool: Pool]>, dependencies: readonly [pool: Token<Pool>]): ServiceCollection (+4 overloads)

Registers a singleton: one instance for the root provider and all its scopes.

  • addSingleton(Class) / addSingleton(Class, [deps]): the class is its own token.
  • addSingleton(token, Implementation, [deps]): deps are checked against the constructor's parameters.
  • addSingleton(token, (provider) => instance): a factory, may be async.

addSingleton
(
class Repo
Repo
, [
class Pool
Pool
])
.
ServiceCollection.buildServiceProvider(options?: BuildOptions): ServiceProvider

Builds the provider. Registrations are copied, so changing this collection afterwards does not affect providers already built.

buildServiceProvider
();
await
const provider: ServiceProvider
provider
.
ServiceProvider.getRequiredService<Repo>(token: Token<Repo>): Promise<Repo>

Resolves token, throwing ServiceNotFoundError if it is not registered.

getRequiredService
(
class Repo
Repo
);
await
const provider: ServiceProvider
provider
.
ServiceProvider.dispose(): Promise<void>

Disposes every instance this provider (or scope) created, in reverse creation order: onDestroy(), else [Symbol.asyncDispose](), else [Symbol.dispose](). Disposing the root disposes singletons; disposing a scope disposes its scoped and transient instances. Every instance is disposed even if some throw; the errors are rethrown afterwards (an AggregateError if more than one). Later calls wait for the first one and do not rethrow.

dispose
();
var console: Console

The console module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers.

The module exports two specific components:

  • A Console class with methods such as console.log(), console.error() and console.warn() that can be used to write to any Node.js stream.
  • A global console instance configured to write to process.stdout and process.stderr. The global console can be used without importing the node:console module.

Warning: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the note on process I/O for more information.

Example using the global console:

console.log('hello world');
// Prints: hello world, to stdout
console.log('hello %s', 'world');
// Prints: hello world, to stdout
console.error(new Error('Whoops, something bad happened'));
// Prints error message and stack trace to stderr:
// Error: Whoops, something bad happened
// at [eval]:5:15
// at Script.runInThisContext (node:vm:132:18)
// at Object.runInThisContext (node:vm:309:38)
// at node:internal/process/execution:77:19
// at [eval]-wrapper:6:22
// at evalScript (node:internal/process/execution:76:60)
// at node:internal/main/eval_string:23:3
const name = 'Will Robinson';
console.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to stderr

Example using the Console class:

const out = getStreamSomehow();
const err = getStreamSomehow();
const myConsole = new console.Console(out, err);
myConsole.log('hello world');
// Prints: hello world, to out
myConsole.log('hello %s', 'world');
// Prints: hello world, to out
myConsole.error(new Error('Whoops, something bad happened'));
// Prints: [Error: Whoops, something bad happened], to err
const name = 'Will Robinson';
myConsole.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to err

@see ― source

console
.
Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)

Prints to stdout with newline. Multiple arguments can be passed, with the first used as the primary message and all additional used as substitution values similar to printf(3) (the arguments are all passed to util.format()).

const count = 5;
console.log('count: %d', count);
// Prints: count: 5, to stdout
console.log('count:', count);
// Prints: count: 5, to stdout

See util.format() for more information.

@since ― v0.1.100

log
(
const log: string[]
log
); // ['Repo', 'Pool', 'Logger']

Each instance is disposed by the provider that owns it:

Instance Disposed by
Singleton The root provider
Scoped The scope that created it
Transient resolved from a scope That scope
Transient resolved from the root The root provider
Value from addValue Never: the container didn’t create it

Every instance is disposed even if some of them throw. After the last one, the error is rethrown: the error itself if one instance failed, or an AggregateError with all of them if several did.

try {
await
const provider: ServiceProvider
provider
.
ServiceProvider.dispose(): Promise<void>

Disposes every instance this provider (or scope) created, in reverse creation order: onDestroy(), else [Symbol.asyncDispose](), else [Symbol.dispose](). Disposing the root disposes singletons; disposing a scope disposes its scoped and transient instances. Every instance is disposed even if some throw; the errors are rethrown afterwards (an AggregateError if more than one). Later calls wait for the first one and do not rethrow.

dispose
();
} catch (
var error: unknown
error
) {
const
const errors: any[]
errors
=
var error: unknown
error
instanceof
var AggregateError: AggregateErrorConstructor
AggregateError
?
var error: AggregateError
error
.
AggregateError.errors: any[]
errors
: [
var error: unknown
error
];
for (const
const e: any
e
of
const errors: any[]
errors
)
var console: Console

The console module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers.

The module exports two specific components:

  • A Console class with methods such as console.log(), console.error() and console.warn() that can be used to write to any Node.js stream.
  • A global console instance configured to write to process.stdout and process.stderr. The global console can be used without importing the node:console module.

Warning: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the note on process I/O for more information.

Example using the global console:

console.log('hello world');
// Prints: hello world, to stdout
console.log('hello %s', 'world');
// Prints: hello world, to stdout
console.error(new Error('Whoops, something bad happened'));
// Prints error message and stack trace to stderr:
// Error: Whoops, something bad happened
// at [eval]:5:15
// at Script.runInThisContext (node:vm:132:18)
// at Object.runInThisContext (node:vm:309:38)
// at node:internal/process/execution:77:19
// at [eval]-wrapper:6:22
// at evalScript (node:internal/process/execution:76:60)
// at node:internal/main/eval_string:23:3
const name = 'Will Robinson';
console.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to stderr

Example using the Console class:

const out = getStreamSomehow();
const err = getStreamSomehow();
const myConsole = new console.Console(out, err);
myConsole.log('hello world');
// Prints: hello world, to out
myConsole.log('hello %s', 'world');
// Prints: hello world, to out
myConsole.error(new Error('Whoops, something bad happened'));
// Prints: [Error: Whoops, something bad happened], to err
const name = 'Will Robinson';
myConsole.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to err

@see ― source

console
.
Console.error(message?: any, ...optionalParams: any[]): void (+1 overload)

Prints to stderr with newline. Multiple arguments can be passed, with the first used as the primary message and all additional used as substitution values similar to printf(3) (the arguments are all passed to util.format()).

const code = 5;
console.error('error #%d', code);
// Prints: error #5, to stderr
console.error('error', code);
// Prints: error 5, to stderr

If formatting elements (e.g. %d) are not found in the first string then util.inspect() is called on each argument and the resulting string values are concatenated. See util.format() for more information.

@since ― v0.1.100

error
('dispose failed:',
const e: any
e
);
}

The provider counts as disposed either way: further resolutions throw DisposedError. Calling dispose() again, or concurrently (two shutdown hooks, await using plus an explicit call), waits for the first disposal to finish and resolves without rethrowing its errors: only the first caller sees them.