Errors and messages
One section per compile-time message family and per runtime DI_BAG_* code: when it appears, the cause, the fix, and the recipe that applies.
A library-created runtime error has a stable code and frozen details; branch on those. Its message has the form <code>: <message>; see https://dany-fedorov.github.io/di-bag/agent/errors.html#<fragment>, where the fragment is the code lower-cased with _ replaced by -. Errors thrown by your factories and disposers keep their identity.
A private binding of a module built with buildModule(keys, { label }) appears as <label>/<key> in messages and details paths (outer/inner/key when nested), which names the module directory to open.
A compile-time rejection is an assignability error whose type reads Unsatisfied<"message", details>. The message ends with ; see https://dany-fedorov.github.io/di-bag/agent/errors.html#<family>, one of the sections below. Put builder.verifyGraph() satisfies void; on its own line to report it there, and set "noErrorTruncation": true to print the details.
Compile-time messages
Missing service
When: build(), verifyGraph(), or check.ts reports required service registrations are missing: <keys>; see https://dany-fedorov.github.io/di-bag/agent/errors.html#missing-service.
Cause: a factory declares a dependency that no registration, installed module, or host supplies. A module's unmet dependencies become requirements of the builder that installs it.
Fix: register each listed key in the host, or a typed fixture in check.ts and tests.
// expect-error: required service registrations are missing: config; see https://dany-fedorov.github.io/di-bag/agent/errors.html#missing-service
import { DiBag } from 'di-bag';
DiBag.createBuilder()
.register({ greeter: ({ config }: { config: { greeting: string } }) => config.greeting })
.verifyGraph() satisfies void;import { DiBag } from 'di-bag';
DiBag.createBuilder()
.register({
config: () => ({ greeting: 'Hello' }),
greeter: ({ config }: { config: { greeting: string } }) => config.greeting,
})
.verifyGraph() satisfies void;Recipe: debug a missing-dependency rejection.
Unsatisfied consumer
When: verifyGraph() reports provided service does not satisfy its consumer dependency; see https://dany-fedorov.github.io/di-bag/agent/errors.html#unsatisfied-consumer, with details { consumer, dependency, expected, provided }, or any registering call (contribute, installModule, register, replace, fork, createScope) and verifyGraph() report contribution service is incompatible with its consumer dependency contract; see https://dany-fedorov.github.io/di-bag/agent/errors.html#unsatisfied-consumer, with details { failures: { consumer, diagnostic } } where diagnostic carries the same four fields for the contributed service.
Cause: a registered service's type is not assignable to the type a consumer declares for it, often after one branch changed a contract.
Fix: change the provider's output or the consumer's declared type so they agree; the details name both keys and both types.
// expect-error: provided service does not satisfy its consumer dependency; see https://dany-fedorov.github.io/di-bag/agent/errors.html#unsatisfied-consumer
import { DiBag } from 'di-bag';
DiBag.createBuilder()
.register({
port: () => 'eighty',
server: ({ port }: { port: number }) => port + 1,
})
.verifyGraph() satisfies void;Recipe: review a merge.
Root capture
When: root lifetime cannot capture scoped dependency: <root> -> <scoped>; see https://dany-fedorov.github.io/di-bag/agent/errors.html#root-capture.
Cause: a root service would keep one scope's instance of a scoped (the default) dependency for the whole application.
Fix: make the dependency root as well, or leave the consumer scoped. Use { allowScopedDependencies: true } only for a deliberate capture of the root bag's instance.
// expect-error: root lifetime cannot capture scoped dependency: client -> config; see https://dany-fedorov.github.io/di-bag/agent/errors.html#root-capture
import { DiBag } from 'di-bag';
DiBag.createBuilder()
.register({
config: () => ({ url: 'memory:' }),
client: DiBag.withLifetime(({ config }: { config: { url: string } }) => config.url, 'root'),
})
.verifyGraph() satisfies void;import { DiBag } from 'di-bag';
DiBag.createBuilder()
.register({
config: DiBag.withLifetime(() => ({ url: 'memory:' }), 'root'),
client: DiBag.withLifetime(({ config }: { config: { url: string } }) => config.url, 'root'),
})
.verifyGraph() satisfies void;Recipe: add and consume an async client.
Unknown key
When: fork accepts existing names or typed tokens only: unknown <key>, the same message for createScope and buildAndStart, replace requires one existing singleton string-literal key: <key>, or, on resolve, inspect, or replace, token must be an individually known genuine handle or token must match an existing binding contract, or <op> requires a finite tuple of singleton string-literal names or typed tokens when the selection is a string[], a union, or a widened array (fork, createScope, createScope share, buildModule, buildAndStart), each followed by ; see https://dany-fedorov.github.io/di-bag/agent/errors.html#unknown-key.
Cause: the selected or resolved key is not registered in this graph, or is a private name of an installed module, or the key is registered under a different typed token than the one passed.
Fix: select only exported or registered keys; add the registration first when the key is new. Pass the selection as a literal tuple (['a', 'b'] as const, or a const type parameter), not a string[].
// expect-error: fork accepts existing names or typed tokens only: unknown host; see https://dany-fedorov.github.io/di-bag/agent/errors.html#unknown-key
import { DiBag } from 'di-bag';
const app = DiBag.createBuilder().register({ port: () => 80 }).build();
app.fork(['host'], { host: () => 'localhost' });Recipe: write a fixture test with fork.
Structural thenable
When: factory output is a structural thenable: <keys>; return a native Promise or use DiBag.fromFactory with acquisitionMode raw or nativePromise; see https://dany-fedorov.github.io/di-bag/agent/errors.html#structural-thenable, or on DiBag.fromFactory, fromFunction, and fromClassfactory output is a structural thenable; return a native Promise or select acquisitionMode raw or nativePromise; see https://dany-fedorov.github.io/di-bag/agent/errors.html#structural-thenable.
Cause: a factory returns an object with a then method that is not a native Promise, such as a query builder. Automatic acquisition cannot tell whether to await it.
Fix: convert it to a native Promise, or keep the object as the service with acquisitionMode: 'raw'.
// expect-error: factory output is a structural thenable: query; return a native Promise or use DiBag.fromFactory with acquisitionMode raw or nativePromise; see https://dany-fedorov.github.io/di-bag/agent/errors.html#structural-thenable
import { DiBag } from 'di-bag';
type Query = { then(onFulfilled: (rows: string[]) => void): void };
const select = (): Query => ({ then: onFulfilled => onFulfilled([]) });
DiBag.createBuilder().register({ query: select }).build();import { DiBag } from 'di-bag';
type Query = { then(onFulfilled: (rows: string[]) => void): void };
const select = (): Query => ({ then: onFulfilled => onFulfilled([]) });
DiBag.createBuilder()
.register({
rows: () => new Promise<string[]>(resolve => select().then(resolve)),
query: DiBag.fromFactory(select, { acquisitionMode: 'raw' }),
})
.build();Recipe: add and consume an async client.
Portable factory output
When: fromSyncFactory output must not be a Promise or thenable; use fromAsyncFactory for a Promise, or fromFactory with acquisitionMode raw to make the Promise object the service; see https://dany-fedorov.github.io/di-bag/agent/errors.html#portable-factory-output, or fromAsyncFactory requires a Promise output; use fromSyncFactory for a synchronous value; see https://dany-fedorov.github.io/di-bag/agent/errors.html#portable-factory-output.
Cause: the helper fixes the acquisition mode from its name, so the factory's declared output must agree with it. fromSyncFactory is a raw stage that never reads then: an async function, a Promise-returning function, a union with a Promise member, or a thenable such as a query builder cannot be its service. fromAsyncFactory is a nativePromise stage: a plain value, a union, or a PromiseLike cannot be its service.
Fix: pick the helper that matches the output. When the Promise object itself is the service, use DiBag.fromFactory(create, { acquisitionMode: 'raw' }).
// expect-error: fromSyncFactory output must not be a Promise or thenable
import { DiBag } from 'di-bag';
const config = DiBag.fromSyncFactory(async () => ({ url: 'memory:' }));import { DiBag } from 'di-bag';
const config = DiBag.fromAsyncFactory(async () => ({ url: 'memory:' }));
const ownedPromise = DiBag.fromFactory(() => Promise.resolve({ url: 'memory:' }), { acquisitionMode: 'raw' });Recipe: make a graph portable to browsers and workers.
Wrong shape at a call
When: register, installModule, or replace reports provided service does not satisfy its consumer dependency; see https://dany-fedorov.github.io/di-bag/agent/errors.html#wrong-shape.
Cause: the same mismatch as an unsatisfied consumer. These call sites keep a short message because naming the keys there costs compile time on every valid graph.
Fix: add verifyGraph() satisfies void; after the call to get the consumer, dependency, expected type, and provided type.
// expect-error: provided service does not satisfy its consumer dependency; see https://dany-fedorov.github.io/di-bag/agent/errors.html#wrong-shape
import { DiBag } from 'di-bag';
const app = DiBag.createBuilder().register({ port: () => 80 });
app.register({ server: ({ port }: { port: string }) => port.length });Recipe: debug a missing-dependency rejection.
Wrong override
When: fork or createScope reports override value is not assignable to the original token: <keys>; see https://dany-fedorov.github.io/di-bag/agent/errors.html#wrong-override, or a plain Type 'X' is not assignable to type 'Y' on an override factory.
Cause: an override's service value is not assignable to the type the original registration declares for that key. A fork or scope substitutes a service but cannot change its contract, and its consumers are typed against the original.
Fix: return the original service type (or a subtype) from the override. To change the contract, change the registration in the builder and re-build().
// expect-error: Type 'string' is not assignable to type 'number'
import { DiBag } from 'di-bag';
const app = DiBag.createBuilder().register({ port: () => 80 }).build();
app.fork(['port'], { port: () => 'eighty' });Recipe: write a fixture test with fork.
Runtime codes
DI_BAG_CLASSIFIER_REQUIRED
When: build() or buildAndStart() completes a graph on a host without process.getBuiltinModule: browsers, Web Workers, and other non-Node runtimes. Node, Bun, and Deno never raise it.
Cause: a registration uses automatic acquisition, no native-Promise classifier is configured, and the host offers none. The message and details.bindings name every such registration, sorted, with private module services as <label>/<key>; a direct transformService without an acquisitionMode counts under its registration's name.
Fix: register each named service with DiBag.fromSyncFactory or DiBag.fromAsyncFactory; give fromFunction, fromClass, and direct transformService an explicit acquisitionMode; or configure a trusted classifier with withConfiguration({ runtime: { isNativePromise } }).
import { DiBag } from 'di-bag';
const app = DiBag.createBuilder()
.register({
answer: DiBag.fromSyncFactory(() => 42),
later: DiBag.fromAsyncFactory(async ({ answer }: { answer: number }) => answer * 2),
})
.build();Recipe: make a graph portable to browsers and workers.
DI_BAG_CLEANUP_AFTER_FACTORY
When: factoryCtx.pushDisposer(disposer) throws because the factory that owns the context has already returned or failed. Its projections may still be running; the factory is the boundary, not the whole acquisition.
Cause: the acquisition context escaped its factory and was called later — from the service it produced, from a projection of the registration, or from inside a pushed disposer already running. A context belongs to one running factory, not to the service it produced.
Fix: push inside the factory, immediately after acquiring the resource; own the returned value with DiBag.withDisposal, and give a pushed disposer for that same value a reason check.
import { DiBag } from 'di-bag';
const handle = DiBag.withDisposal(
DiBag.fromFactory(async (_deps: {}, factoryCtx) => {
const socket = { close: async () => {} };
factoryCtx.pushDisposer(disposerCtx => { if (disposerCtx.reason !== 'service-disposed') return socket.close(); });
return socket;
}, { context: 'acquisition' }),
socket => socket.close(),
);Recipe: own a resource a factory acquires on the way.
DI_BAG_CLEANUP_FAILED
When: close() rejects with DiBagCleanupError after attempting every disposer.
Cause: one or more disposers threw or rejected. The others still ran and the bag is closed; failures lists label and error for each.
Fix: fix the failing disposer; log the failures where the application closes.
import { DiBag, DiBagCleanupError } from 'di-bag';
const app = DiBag.createBuilder().register({ answer: () => 42 }).build();
try {
await app.close();
} catch (error) {
if (!(error instanceof DiBagCleanupError)) throw error;
for (const failure of error.failures) console.error(failure.label, failure.error);
}Recipe: add a request-scoped service with cleanup.
DI_BAG_CLOSE_ABORTED
When: close({ signal }) rejects with DiBagCloseCancelledError, reason: 'aborted', because the signal aborted before cleanup finished.
Cause: the caller stopped waiting. Cleanup continues: details.pending names disposers that started and have not finished, details.acquiring the acquisitions close is still draining, and cause is the abort reason.
Fix: await cleanupPromise before exiting when cleanup must complete; fix the named disposer or acquisition if it never settles.
import { DiBag, DiBagCloseCancelledError } from 'di-bag';
const app = DiBag.createBuilder().register({ answer: () => 42 }).build();
const controller = new AbortController();
try {
await app.close({ signal: controller.signal });
} catch (error) {
if (!(error instanceof DiBagCloseCancelledError)) throw error;
console.error(error.details.pending, error.details.acquiring);
await error.cleanupPromise;
}Recipe: add a request-scoped service with cleanup.
DI_BAG_CLOSE_FAILED
When: close() rejects with an AggregateError carrying this code.
Cause: closing a child scope or the bag's own acquisitions failed with something other than disposer failures. errors holds each failure, preceded by a DiBagCleanupError when disposers also failed.
Fix: inspect errors; each entry keeps its own code when the library created it.
import { DiBag, type DiBagDiagnostic } from 'di-bag';
const app = DiBag.createBuilder().register({ answer: () => 42 }).build();
await app.close().catch((error: unknown) => {
const failures = error instanceof AggregateError ? error.errors : [error];
for (const failure of failures) console.error((failure as Partial<DiBagDiagnostic>).code, failure);
});Recipe: add a request-scoped service with cleanup.
DI_BAG_CLOSE_TIMEOUT
When: close({ timeoutMs }) rejects with DiBagCloseCancelledError, reason: 'timeout'; its cause is a TimeoutError with the same code.
Cause: cleanup did not finish within timeoutMs. The message and details.pending name the disposers still running, or details.acquiring the acquisitions still pending; cleanupPromise settles when cleanup ends.
Fix: find why the named disposer or factory never settles (a missing await, an ignored acquisition signal); raise timeoutMs only for slow but finite cleanup.
import { DiBag, DiBagCloseCancelledError } from 'di-bag';
const app = DiBag.createBuilder().register({ answer: () => 42 }).build();
await app.close({ timeoutMs: 5_000 }).catch((error: unknown) => {
if (error instanceof DiBagCloseCancelledError) console.error('still running:', error.details.pending);
throw error;
});Recipe: add a request-scoped service with cleanup.
DI_BAG_CLOSED
When: resolve, createScope, fork, or a lazy reference is used on a bag whose close() has finished.
Cause: application work outlived the bag that serves it. details.state is 'closed'.
Fix: finish or cancel work before closing; give request work its own scope and close the scope, not the application bag.
import { DiBag } from 'di-bag';
const app = DiBag.createBuilder().register({ answer: () => 42 }).build();
const scope = app.createScope();
try {
scope.resolve('answer');
} finally {
await scope.close();
}
await app.close();Recipe: add a request-scoped service with cleanup.
DI_BAG_CLOSING
When: the same operations as DI_BAG_CLOSED, while close() is still in progress. Also the message of factoryCtx.signal.reason after close(): an AbortError that is the same object for every bag. A cancelled or failed startup aborts with its own cause instead.
Cause: a request, timer, or factory started new resolution after shutdown began. Only a factory already running when close() started may still read its dependencies.
Fix: stop accepting work (close the server, clear timers), await in-flight work, then call close(); see the snippet for DI_BAG_CLOSED.
Recipe: add a request-scoped service with cleanup.
DI_BAG_CYCLE
When: resolving a service whose dependencies lead back to it; the message is cycle: a -> b -> a (or alias cycle: ...) and details.path lists the keys.
Cause: factories depend on each other in a loop. The compiler does not detect cycles.
Fix: move the shared part into a third service both depend on, or defer one edge with DiBag.lazy(token).
import { DiBag } from 'di-bag';
const app = DiBag.createBuilder()
.register({
rates: () => ({ vat: 0.2 }),
prices: ({ rates }: { rates: { vat: number } }) => (cents: number) => cents * (1 + rates.vat),
invoices: ({ rates }: { rates: { vat: number } }) => (cents: number) => cents * rates.vat,
})
.build();Recipe: review a merge (di-bag-graph --check reports cycles before running).
DI_BAG_DUPLICATE_METADATA
When: DiBag.withMetadata(registration, { static }) adds a key the registration already carries.
Cause: two metadata wrappers use the same key.
Fix: use distinct, namespaced keys such as 'app:owner' and 'app:node'.
import { DiBag } from 'di-bag';
const service = DiBag.withMetadata(
DiBag.withMetadata(() => 42, { static: { 'app:owner': 'billing' } }),
{ static: { 'app:node': 'tool' } },
);Recipe: none.
DI_BAG_DUPLICATE_REGISTRATION
When: register, alias, or installModule adds a public key that already exists. The compiler reports register introduces new names or typed tokens only.
Cause: two registrations or two installed modules export the same name.
Fix: use replace(key, factory) to substitute an implementation; install a second copy of a module under another name with renameExport.
// expect-error: register introduces new names or typed tokens only
import { DiBag } from 'di-bag';
DiBag.createBuilder().register({ port: () => 80 }).register({ port: () => 81 });import { DiBag } from 'di-bag';
DiBag.createBuilder().register({ port: () => 80 }).replace('port', () => 81).build();Recipe: split a feature into a module.
DI_BAG_INTERNAL_STATE
When: a library invariant failed, for example an acquisition without a result.
Cause: a DI Bag defect, not application code.
Fix: report it at https://github.com/dany-fedorov/di-bag/issues with the stack trace and the smallest graph that reproduces it.
Recipe: none.
DI_BAG_INVALID_ACQUISITION_MODE
When: fromFactory, fromFunction, fromClass, or transformService receives options that are not an object, or an acquisitionMode other than 'auto', 'raw', or 'nativePromise'.
Cause: a misspelled mode or options computed at runtime.
Fix: pass one of the three literals.
import { DiBag } from 'di-bag';
const handle = DiBag.fromFactory(() => Promise.resolve(1), { acquisitionMode: 'raw' });Recipe: add and consume an async client.
DI_BAG_INVALID_ALIAS
When: alias(destination, 'target') names a string target that is not yet registered. The compiler reports alias requires an existing named target.
Cause: the alias is declared before its named target.
Fix: register the target first, or alias a typed token, which may be bound later.
import { DiBag } from 'di-bag';
const app = DiBag.createBuilder()
.register({ service: () => ({ port: 8080 }) })
.alias('primary', 'service')
.build();Recipe: none.
DI_BAG_INVALID_CLASSIFIER_RESULT
When: a factory runs under a configured runtime.isNativePromise that returned something other than a boolean.
Cause: the predicate returns undefined, a truthy value, or a Promise.
Fix: return exactly true or false, without structural then checks.
import { types } from 'node:util';
import { DiBag as CoreDiBag } from 'di-bag';
const DiBag = CoreDiBag.withConfiguration({
runtime: { isNativePromise: value => types.isPromise(value) },
});Recipe: none.
DI_BAG_INVALID_CLEANUP
When: factoryCtx.pushDisposer(disposer) throws because disposer is not a function.
Cause: a value was passed where a disposer callback belongs, usually the result of calling the release instead of passing it.
Fix: pass a function: factoryCtx.pushDisposer(() => socket.close()), not factoryCtx.pushDisposer(socket.close()).
import { DiBag } from 'di-bag';
const socket = DiBag.fromFactory(async (_deps: {}, factoryCtx) => {
const handle = { close: async () => {} };
factoryCtx.pushDisposer(() => handle.close());
return handle;
}, { context: 'acquisition' });Recipe: own a resource a factory acquires on the way.
DI_BAG_INVALID_CLOSE
When: close(options) rejects because options are not { timeoutMs?, signal? } with a finite positive timeoutMs and a genuine AbortSignal. Cleanup does not start.
Cause: options computed at runtime, extra keys, or a zero or negative deadline.
Fix: pass only timeoutMs and signal, or call close() without options.
import { DiBag } from 'di-bag';
const app = DiBag.createBuilder().register({ answer: () => 42 }).build();
await app.close({ timeoutMs: 1_000, signal: AbortSignal.timeout(2_000) });Recipe: add a request-scoped service with cleanup.
DI_BAG_INVALID_CONFIGURATION
When: DiBag.withConfiguration(options) receives a non-object, observers that is not an array, an observer without both onEvent and onError, or a runtime without an isNativePromise function.
Cause: incomplete configuration.
Fix: pass both observer callbacks and a function classifier.
import { DiBag } from 'di-bag';
const observed = DiBag.withConfiguration({
observers: [{ onEvent: event => console.log(event.kind), onError: ({ error }) => console.error(error) }],
});Recipe: none.
DI_BAG_INVALID_CONSTRUCTOR
When: DiBag.fromClass(dependencies, value) receives something that cannot be called with new, such as an arrow function.
Cause: a function passed where a class is expected.
Fix: pass the class; adapt a plain function with fromFunction.
import { DiBag } from 'di-bag';
const portKey = Symbol('port');
const port = DiBag.token(portKey).of<number>();
class Client { constructor(readonly port: number) {} }
const client = DiBag.fromClass([port], Client);
const address = DiBag.fromFunction([port], value => `localhost:${value}`);Recipe: none.
DI_BAG_INVALID_DEPENDENCY_ACCESS
When: a factory spreads its dependency object, enumerates it (Object.keys, JSON.stringify), or uses in or a property descriptor on it. details.consumer names the factory and details.access the operation.
Cause: the dependency object resolves each property lazily when read, so it cannot list its properties.
Fix: destructure the declared dependencies or read them one by one.
import { DiBag } from 'di-bag';
const app = DiBag.createBuilder()
.register({
port: () => 80,
host: () => 'localhost',
address: ({ host, port }: { host: string; port: number }) => ({ host, port }),
})
.build();Recipe: none; see rule 2.
DI_BAG_INVALID_EXPORT
When: buildModule(keys, options) receives a non-array, a key that is not registered on that builder, or a label that is not a non-empty string (details.option: 'label'), or renameExport(old, new) names a missing export, a non-string name, or an existing export.
Cause: the export list and the registrations disagree.
Fix: export only keys the module registers; rename to an unused name.
import { DiBag } from 'di-bag';
const reports = DiBag.createBuilder().register({ service: () => ({ read: () => true }) }).buildModule(['service']);
const east = reports.renameExport('service', 'eastReports');Recipe: split a feature into a module.
DI_BAG_INVALID_FACTORY
When: DiBag.fromFactory, fromSyncFactory, or fromAsyncFactory receives a non-function, or a context option other than 'acquisition'; the two portable helpers also refuse an acquisitionMode option, because they fix it themselves.
Cause: a value passed where a factory is expected, or a mode passed to a helper whose name already selects it.
Fix: pass a function; use { context: 'acquisition' } to receive the acquisition context as the second argument; choose fromSyncFactory or fromAsyncFactory instead of passing a mode to them.
import { DiBag } from 'di-bag';
const settings = DiBag.fromFactory(
async ({ url }: { url: string }, { signal }) => (await fetch(url, { signal })).text(),
{ context: 'acquisition' },
);Recipe: add and consume an async client.
DI_BAG_INVALID_FUNCTION
When: DiBag.fromFunction(dependencies, callback) receives a non-function.
Cause: a value passed where the adapted function is expected.
Fix: pass the function; bind methods that need their receiver.
import { DiBag } from 'di-bag';
const nameKey = Symbol('name');
const name = DiBag.token(nameKey).of<string>();
const greeting = DiBag.fromFunction([name], value => `Hello, ${value}`);Recipe: none.
DI_BAG_INVALID_LIFETIME
When: withLifetime(registration, lifetime, options) receives a lifetime other than 'root', 'scoped', or 'transient', unknown options, or allowScopedDependencies on a non-root lifetime or as a non-boolean.
Cause: a computed or misspelled policy.
Fix: pass a literal lifetime; use allowScopedDependencies: true only with 'root'.
import { DiBag } from 'di-bag';
const config = DiBag.withLifetime(() => ({ region: 'eu' }), 'root');Recipe: add and consume an async client.
DI_BAG_INVALID_METADATA
When: withMetadata receives neither static nor dynamic options, a non-object static record, a dynamic mode other than 'direct' or 'awaited', or a describe callback that is not a function or does not synchronously return a plain object.
Cause: metadata that is not a plain record.
Fix: return a plain object literal from describe.
import { DiBag } from 'di-bag';
const client = DiBag.withMetadata(() => ({ region: 'eu' }), {
dynamic: { mode: 'direct', describe: value => ({ 'app:region': value.region }) },
});Recipe: none.
DI_BAG_INVALID_MODULE
When: installModule(value) receives something that buildModule did not create, such as a copied or proxied module.
Cause: the module was cloned, serialized, or constructed by hand.
Fix: import and install the module value exported by module.ts.
import { DiBag } from 'di-bag';
const feature = DiBag.createBuilder().register({ answer: () => 42 }).buildModule(['answer']);
const app = DiBag.createBuilder().installModule(feature).build();Recipe: split a feature into a module.
DI_BAG_INVALID_OVERRIDE
When: fork(keys, overrides) receives a non-array selection, a non-object override record, a key that is not registered, or a selected key without an own override property.
Cause: the selection and the override object disagree. The compiler reports unknown key for literal selections.
Fix: list each replaced key once and give it an override.
import { DiBag } from 'di-bag';
type Clock = { now(): number };
const app = DiBag.createBuilder().register({ clock: (): Clock => ({ now: () => 42 }) }).build();
const testApp = app.fork(['clock'], { clock: () => ({ now: () => 7 }) });
await testApp.close();Recipe: write a fixture test with fork.
DI_BAG_INVALID_PLUGIN_OPTIONS
When: DiBag.fromPlugin(dependencies, descriptor, options) receives options without an own acquisitionMode of 'raw' or 'nativePromise', or without a validate function.
Cause: plugin output must be validated and its acquisition mode chosen.
Fix: pass both options.
import { DiBag } from 'di-bag';
type Handler = { handle(text: string): string };
const descriptor: unknown = { apiVersion: 1, create: () => ({ handle: (text: string) => text }) };
const handler = DiBag.fromPlugin([], descriptor, {
acquisitionMode: 'raw',
validate: (value: unknown): value is Handler => typeof value === 'object' && value !== null && 'handle' in value,
});Recipe: none.
DI_BAG_INVALID_REGISTRATION
When: register receives a non-object, a record with symbol keys, or a value that is neither a factory nor a DiBag provider.
Cause: a constant registered directly, or tokens mixed into a name record.
Fix: wrap values in factories; register tokens with register(token, provider).
import { DiBag } from 'di-bag';
const portKey = Symbol('port');
const port = DiBag.token(portKey).of<number>();
DiBag.createBuilder().register({ host: () => 'localhost' }).register(port, () => 80).build();Recipe: none.
DI_BAG_INVALID_REPLACEMENT
When: replace(key, registration) names a key the builder does not expose. The compiler reports unknown key.
Cause: the key is misspelled, not yet registered, or private to a module.
Fix: replace an exported or registered key; register a new one instead.
import { DiBag } from 'di-bag';
DiBag.createBuilder().register({ port: () => 80 }).replace('port', () => 8080).build();Recipe: write a fixture test with fork.
DI_BAG_INVALID_SCOPE
When: createScope receives more than three arguments, a non-array selection, a non-object override record, options other than { share }, an unregistered key, a key both shared and overridden, a shared transient service, or a selected key without an override.
Cause: the selection, overrides, and sharing disagree. The compiler reports most of these, for example createScope cannot share transient providers.
Fix: override and share disjoint, registered, non-transient keys.
import { DiBag } from 'di-bag';
const parent = DiBag.createBuilder()
.register({ config: () => ({ region: 'eu' }), client: () => ({ id: 1 }) })
.build();
const child = parent.createScope(['config'], { config: () => ({ region: 'us' }) }, { share: ['client'] });
await parent.close();Recipe: add a request-scoped service with cleanup.
DI_BAG_INVALID_STARTUP
When: buildAndStart(keys, options) receives a non-array selection, an unregistered key, unknown options, a non-positive timeoutMs, an invalid startupOrder, or a signal that is not an AbortSignal. No factory runs.
Cause: startup options computed at runtime.
Fix: pass registered keys and valid options.
import { DiBag } from 'di-bag';
const app = await DiBag.createBuilder()
.register({ settings: async () => 'ready' })
.buildAndStart(['settings'], { timeoutMs: 5_000, startupOrder: 'sequential' });
await app.close();Recipe: add and consume an async client.
DI_BAG_INVALID_TOKEN
When: DiBag.token(key) receives a non-symbol, a token argument is a copied or fabricated object, or a dependency list is not an array.
Cause: token identity comes from the handle token(key).of() returns, not from its shape.
Fix: declare the symbol and token once, export the token, and import it wherever it is used.
import { DiBag } from 'di-bag';
const clockKey = Symbol('clock');
export const clock = DiBag.token(clockKey).of<{ now(): number }>();Recipe: none.
DI_BAG_INVALID_TRANSFORM
When: transformService(registration, options) receives a mode other than 'direct' or 'awaited', no transform function, or acquisitionMode with 'awaited'.
Cause: options that do not match the transform mode.
Fix: pass acquisitionMode only with 'direct'.
import { DiBag } from 'di-bag';
const upper = DiBag.transformService(async () => 'ready', { mode: 'awaited', transform: value => value.toUpperCase() });Recipe: none.
DI_BAG_LIFETIME_DEPENDENCY
When: a root service resolves a scoped dependency at runtime; details.consumer and details.dependency name both.
Cause: the root capture check was bypassed by a cast or untyped code.
Fix: as for root capture: make the dependency root, or the consumer scoped.
Recipe: add and consume an async client.
DI_BAG_MISSING_DEPENDENCY
When: a factory reads a dependency that no registration supplies. The message is Cannot resolve "<consumer>": dependency "<key>" is not registered, and details.path is the resolution chain.
Cause: a cast, any, or JavaScript hid the dependency from the missing service check.
Fix: remove the cast so the compiler reports the key, then register it.
Recipe: debug a missing-dependency rejection.
DI_BAG_MISSING_REGISTRATION
When: resolve(key) names a key the bag does not expose; the message is Service "<key>" is not registered.
Cause: a key computed at runtime or cast to a registered name; module private names are not public.
Fix: resolve literal exported keys; bag.inspectGraph() lists each binding's public keys.
import { DiBag } from 'di-bag';
const app = DiBag.createBuilder().register({ port: () => 80 }).build();
const keys = app.inspectGraph().bindings.flatMap(binding => binding.keys);Recipe: debug a missing-dependency rejection.
DI_BAG_PLUGIN_VALIDATION
When: a fromPlugin provider acquires; DiBagPluginValidationError with phase: 'descriptor' or 'output' and a reason.
Cause: the descriptor lacks own apiVersion: 1 and a callable create, or validate did not return exactly true for the output.
Fix: correct the plugin, or reject it before registering; see DI_BAG_INVALID_PLUGIN_OPTIONS for a valid descriptor.
Recipe: none.
DI_BAG_STARTUP_CANCELLED
When: buildAndStart rejects with DiBagStartupCancelledError, reason'aborted' or 'timeout'.
Cause: the external signal aborted or timeoutMs elapsed before the selected services were ready. Cleanup continues in the background.
Fix: await cleanupPromise before exiting; make slow factories honor the acquisition signal.
import { DiBag, DiBagStartupCancelledError } from 'di-bag';
const builder = DiBag.createBuilder().register({ settings: async () => 'ready' });
try {
await (await builder.buildAndStart(['settings'], { timeoutMs: 5_000 })).close();
} catch (error) {
if (error instanceof DiBagStartupCancelledError) await error.cleanupPromise;
throw error;
}Recipe: add and consume an async client.
DI_BAG_STARTUP_FAILED
When: buildAndStart rejects with DiBagStartupError after rolling back the new bag.
Cause: a selected service or its dependency failed to acquire; cause is that error and cleanupFailures lists rollback disposer failures.
Fix: fix cause; startup can be retried with a new buildAndStart.
import { DiBag, DiBagStartupError } from 'di-bag';
const builder = DiBag.createBuilder().register({ settings: async () => 'ready' });
const app = await builder.buildAndStart(['settings']).catch((error: unknown) => {
throw error instanceof DiBagStartupError ? error.cause : error;
});
await app.close();Recipe: add and consume an async client.
DI_BAG_STARTUP_TIMEOUT
When: the cause of a DI_BAG_STARTUP_CANCELLED error with reason: 'timeout': a DOMException named TimeoutError, with details.timeoutMs.
Cause: selected services took longer than timeoutMs.
Fix: raise timeoutMs, start fewer services eagerly, or make factories honor the signal so they stop promptly.
Recipe: add and consume an async client.
DI_BAG_STRUCTURAL_THENABLE
When: a factory with automatic or native acquisition returns a non-Promise object with a callable then; a TypeError with details.acquisitionMode.
Cause: the compile-time structural thenable check was disabled through DiBagPolicy or bypassed by a cast.
Fix: as for the compile-time message: return a native Promise or use acquisitionMode: 'raw'.
Recipe: add and consume an async client.