Skip to content
Buy me a coffee

Validation

Two options of buildServiceProvider find wiring mistakes early. Both are off by default, like in .NET, where ValidateScopes and ValidateOnBuild are switched on in development.

const
const provider: ServiceProvider
provider
=
const services: ServiceCollection
services
.
ServiceCollection.buildServiceProvider(options?: BuildOptions): ServiceProvider

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

buildServiceProvider
({
BuildOptions.validateScopes?: boolean | undefined

Throw a ScopeError when a scoped service is resolved from the root provider or captured by a singleton. Recommended in development.

validateScopes
: true, // enforce lifetime rules while resolving
BuildOptions.validateOnBuild?: boolean | undefined

Check every registration up front (missing dependencies, captive scoped services) and throw a ValidationError.

validateOnBuild
: true, // check every registration now
});
Option Checks When Throws
validateScopes Scoped resolved from the root; scoped captured by a singleton On every resolution ScopeError
validateOnBuild Every dependency is registered In buildServiceProvider() ValidationError
both The above, plus every singleton that would capture a scoped service In buildServiceProvider() ValidationError

Turn both on in development and in tests. In production they cost little: validateOnBuild runs once at startup, and validateScopes adds a short walk of the current resolution chain.

A scoped service resolved from the root provider is cached on the root and lives until shutdown: in effect a singleton, holding whatever per-request state it saw first.

class
class RequestContext
RequestContext
{}
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<RequestContext>(token: Newable<RequestContext, []>): ServiceCollection (+4 overloads)

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

addSingleton

.

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

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

buildServiceProvider
({
BuildOptions.validateScopes?: boolean | undefined

Throw a ScopeError when a scoped service is resolved from the root provider or captured by a singleton. Recommended in development.

validateScopes
: true });
await
const provider: ServiceProvider
provider
.
ServiceProvider.getRequiredService<RequestContext>(token: Token<RequestContext>): Promise<RequestContext>

Resolves token, throwing ServiceNotFoundError if it is not registered.

getRequiredService
(
class RequestContext
RequestContext
);
// ScopeError: Scoped service 'RequestContext' cannot be resolved from the root provider.
// Create a scope first: `await using scope = provider.createScope();`

A singleton that depends on a scoped service (directly or through transients) captures the first scope’s instance and hands it to every later request. That is a captive dependency:

class
class DbTransaction
DbTransaction
{}
class
class ReportCache
ReportCache
{
constructor(readonly
ReportCache.tx: DbTransaction
tx
:
class DbTransaction
DbTransaction
) {}
}
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<DbTransaction>(token: Newable<DbTransaction, []>): ServiceCollection (+4 overloads)

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

addSingleton

.

addScoped
(
class DbTransaction
DbTransaction
)
.
ServiceCollection.addSingleton<ReportCache, [tx: DbTransaction]>(token: Newable<ReportCache, [tx: DbTransaction]>, dependencies: readonly [tx: Token<DbTransaction>]): 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 ReportCache
ReportCache
, [
class DbTransaction
DbTransaction
])
.
ServiceCollection.buildServiceProvider(options?: BuildOptions): ServiceProvider

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

buildServiceProvider
({
BuildOptions.validateScopes?: boolean | undefined

Throw a ScopeError when a scoped service is resolved from the root provider or captured by a singleton. Recommended in development.

validateScopes
: true });
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<ReportCache>(token: Token<ReportCache>): Promise<ReportCache>

Resolves token, throwing ServiceNotFoundError if it is not registered.

getRequiredService
(
class ReportCache
ReportCache
);
// ScopeError: Scoped service 'DbTransaction' cannot be injected into singleton 'ReportCache'
// (ReportCache → DbTransaction): the singleton would keep one scope's instance forever.

The message shows the path from the singleton to the scoped service, so captures through intermediate transients are easy to trace. This check also covers dependencies resolved inside factories, because it runs during resolution.

The fix is one of: make the consumer scoped, make the dependency a singleton (if it holds no per-request state), or resolve the scoped service per call from a scope instead of injecting it.

Checks every registration (plain and keyed) when the provider is built and throws one ValidationError that lists every problem, not just the first. error.errors holds them as an array of strings.

interface
interface Mailer
Mailer
{
Mailer.send(to: string): Promise<void>
send
(
to: string
to
: string):
interface Promise<T>

Represents the completion of an asynchronous operation

Promise
<void>;
}
const
const Mailer: InjectionToken<Mailer>
Mailer
=
createToken<Mailer>(name: string): InjectionToken<Mailer>

Creates a typed token.

@example

interface Logger { log(msg: string): void }
const Logger = createToken<Logger>('Logger');
services.addSingleton(Logger, ConsoleLogger);
const logger = await provider.getRequiredService(Logger); // typed as Logger

createToken
<
interface Mailer
Mailer
>('Mailer');
class
class DbTransaction
DbTransaction
{}
class
class SignupService
SignupService
{
constructor(readonly
SignupService.mailer: Mailer
mailer
:
interface Mailer
Mailer
, readonly
SignupService.tx: DbTransaction
tx
:
class DbTransaction
DbTransaction
) {}
}
const
const services: ServiceCollection
services
= 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<DbTransaction>(token: Newable<DbTransaction, []>): ServiceCollection (+4 overloads)

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

addSingleton

.

addScoped
(
class DbTransaction
DbTransaction
)
.
ServiceCollection.addSingleton<SignupService, [mailer: Mailer, tx: DbTransaction]>(token: Newable<SignupService, [mailer: Mailer, tx: DbTransaction]>, dependencies: readonly [mailer: Token<Mailer>, tx: Token<DbTransaction>]): 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 SignupService
SignupService
, [
const Mailer: InjectionToken<Mailer>
Mailer
,
class DbTransaction
DbTransaction
]); // Mailer never registered
try {
const services: ServiceCollection
services
.
ServiceCollection.buildServiceProvider(options?: BuildOptions): ServiceProvider

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

buildServiceProvider
({
BuildOptions.validateOnBuild?: boolean | undefined

Check every registration up front (missing dependencies, captive scoped services) and throw a ValidationError.

validateOnBuild
: true,
BuildOptions.validateScopes?: boolean | undefined

Throw a ScopeError when a scoped service is resolved from the root provider or captured by a singleton. Recommended in development.

validateScopes
: true });
} catch (
var error: unknown
error
) {
if (
var error: unknown
error
instanceof
class ValidationError

validateOnBuild found problems. Every problem is listed in

ValidationError.errors

.

ValidationError
)
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
(
var error: ValidationError
error
.
ValidationError.errors: readonly string[]
errors
);
// [
// "'SignupService' depends on 'Mailer', which is not registered.",
// "Singleton 'SignupService' captures scoped 'DbTransaction' (SignupService → DbTransaction)."
// ]
}

The captive-dependency check runs at build time only when validateScopes is on as well. With validateOnBuild alone you get the missing-dependency check.

The graph below runs the same analysis on an editable set of registrations: captive edges are marked from the singleton down to the scoped service it would capture.

6registrations
0cycles
2captive
0missing
DbSIRequestContextSCSessionSCTemplatesTRCacheSIMailerSI
SI singletonSC scopedTR transient cycle captive dependencyClick a service to resolve it for real.
ValidationError: Singleton 'Cache' captures scoped 'Session' (Cache → Session).
ValidationError: Singleton 'Mailer' captures scoped 'RequestContext' (Mailer → Templates → RequestContext).

A cheap, valuable test: build the real container with both options on. It fails as soon as someone adds a dependency without registering it.

function it(name: string, fn: () => void | Promise<void>): void
it
('wires the container', () => {
const
const services: ServiceCollection
services
= 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
();
function registerServices(services: ServiceCollection): void
registerServices
(
const services: ServiceCollection
services
);
const services: ServiceCollection
services
.
ServiceCollection.buildServiceProvider(options?: BuildOptions): ServiceProvider

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

buildServiceProvider
({
BuildOptions.validateOnBuild?: boolean | undefined

Check every registration up front (missing dependencies, captive scoped services) and throw a ValidationError.

validateOnBuild
: true,
BuildOptions.validateScopes?: boolean | undefined

Throw a ScopeError when a scoped service is resolved from the root provider or captured by a singleton. Recommended in development.

validateScopes
: true });
});

See Testing for more.