Mental model. Every error the library throws extends one abstract class, DiError, and carries a machine-readable
code plus the context fields a human needs to act. A catch (error) { if (error instanceof DiError) … } catches all
of them; a switch on code tells them apart without string-matching messages.
Normative. Every error extends
DiError— an abstract class that forces each subclass to declare acodestring (machine-readable), alongside a message carrying enough context for a human reader.
| Error | code |
Thrown when | Context fields |
|---|---|---|---|
InternalError |
INTERNAL_ERROR |
An internal assertion failed — not a user error | — |
TokenNotBoundError |
TOKEN_NOT_BOUND |
The token has no binding at all, even after walking the parent chain | tokenName, path |
NoMatchingBindingError |
NO_MATCHING_BINDING |
The token has bindings but no slot matches the hint | tokenName, options, availableSlots, path |
AmbiguousBindingError |
AMBIGUOUS_BINDING |
≥ 2 candidates remain and the more-specific rule cannot decide | tokenName, candidateIds |
CircularDependencyError |
CIRCULAR_DEPENDENCY |
A → B → A, including a cycle along an alias chain | cycle |
AsyncResolutionError |
ASYNC_RESOLUTION |
A sync resolve() on an async binding, directly or via the dep chain |
tokenName, asyncSourceToken |
AsyncActivationError |
ASYNC_ACTIVATION |
@postConstruct or onActivation returned a Promise on a sync path |
tokenName, hookKind, methodName |
AsyncDeactivationError |
ASYNC_DEACTIVATION |
A sync unbind()/rebind() owing an async deactivation (a cached singleton) |
tokenName |
ScopeViolationError |
SCOPE_VIOLATION |
validate() — a captive dependency: a singleton depending on scoped/transient |
details: both tokens + scopes, plus path |
MissingMetadataError |
MISSING_METADATA |
The container must construct a class but @injectable() is missing |
targetName |
InvalidMetadataError |
INVALID_METADATA |
The MetadataReader returned something the container cannot use |
targetName, reason |
AsyncModuleLoadError |
ASYNC_MODULE_LOAD |
A sync load() received an AsyncModule |
moduleName |
MissingScopeContextError |
MISSING_SCOPE_CONTEXT |
A scoped binding resolved from a container with no child scope |
tokenName |
MissingContainerContextError |
MISSING_CONTAINER_CONTEXT |
A class with @inject accessor was new-ed outside a container |
className (may be undefined), accessorName |
RebindUnboundTokenError |
REBIND_UNBOUND_TOKEN |
rebind() on a token with no own binding in this container |
tokenName |
DisposedContainerError |
DISPOSED_CONTAINER |
Any operation on an already-disposed container | — |
ChainNotRegisteredError |
CHAIN_NOT_REGISTERED |
A scope, on* or id() called before to*() |
tokenName |
ChainAlreadyRegisteredError |
CHAIN_ALREADY_REGISTERED |
A slot step or a second to*() on a chain that already registered its binding |
tokenName |
ManyBindingSlotError |
MANY_BINDING_SLOT |
many() on a named or tagged binding, or a slot constraint on a member |
tokenName |
SelfBindingRequiresClassError |
SELF_BINDING_REQUIRES_CLASS |
toSelf() on a token that is not a class |
tokenName |
StaticMemberDecoratorError |
STATIC_MEMBER_DECORATOR |
@inject / @postConstruct / @preDestroy on a static member |
decoratorName, memberName |
SymbolKeyedLifecycleError |
SYMBOL_KEYED_LIFECYCLE |
@postConstruct / @preDestroy on a symbol-keyed method |
decoratorName, memberName |
PrivateLifecycleMethodError |
PRIVATE_LIFECYCLE_METHOD |
@postConstruct / @preDestroy on a private (#name) method |
decoratorName, memberName |
InvalidBindingDeclarationError |
INVALID_BINDING_DECLARATION |
binding() — no strategy, several, or a key the strategy does not allow |
tokenName, reason |
MissingDecoratorMetadataError |
MISSING_DECORATOR_METADATA |
A decorator handed no context.metadata — the runtime lacks Symbol.metadata |
decoratorName |
UnreachableLifecycleHookError |
UNREACHABLE_LIFECYCLE_HOOK |
validate() — a container-level hook for a token nobody binds |
tokenName, phase, reason |
EmptyTagCriteriaError |
EMPTY_TAG_CRITERIA |
…TaggedAll() received an empty criterion list |
helperName |
UnreachableConstraintError |
UNREACHABLE_CONSTRAINT |
validate() — a constraint expects a slot name nobody declares |
tokenName, requiredName, requiredTokenName, helperName |
Exact shape:
src/errors.ts— every class above, plusScopeViolationDetails.
A message names what went wrong and, where the library can know it, the way out — every misuse error does; a few report
a state rather than a remedy (CircularDependencyError names the cycle, ScopeViolationError the path,
DisposedContainerError the disposal). Two representative examples:
No binding for 'app:Logger' matching {"name":"file"}. Available slots: [default, name:console].
Token 'app:Api' requires async resolution because 'app:Database' in its dependency
chain has an async factory. Use container.resolveAsync(app:Api).TokenNotBoundError and NoMatchingBindingError carry path: the token names from the outermost request down to the
one that missed, alias hops included — [tokenName] for a miss on the token asked for, which the message then leaves
out. Their message ends with Path: a → b → c otherwise. validate() raises both, and CircularDependencyError, for a
singleton graph that cannot resolve, with the path from that singleton.
ScopeViolationError is raised by validate() alone: a resolve does not check scopes, so a singleton that captures a
transient resolves silently, and one that captures a scoped binding from the root fails with
MissingScopeContextError instead.
The boundary between a library bug and a caller error
Normative.
InternalErrormeans the library is broken — a consumer catching one has caught a library bug. No user-caused error may therefore carry that type.
Three errors in the table exist precisely because of that rule: AmbiguousBindingError (predicates that do not exclude
each other), StaticMemberDecoratorError, and ChainNotRegisteredError — each is caller misuse, so each has its own
type rather than InternalError.
Errors for callers outside the type system. ChainNotRegisteredError and SelfBindingRequiresClassError are nearly
unreachable from TypeScript: the chain's return types
(Fluent chain) and the type of bind() already block most of
the way. They exist for JavaScript callers, or callers who have cast through the types — so that misuse fails loudly
instead of silently doing nothing — and they belong to the DiError taxonomy so that a
catch (error) { if (error instanceof DiError) … } does not let them escape.
Why static members are an error. StaticMemberDecoratorError exists because all three of those decorators act on
an instance: @inject resolves through the container active while the instance is being constructed, and
@postConstruct/@preDestroy bracket one instance's lifecycle. A static member belongs to the class, and the container
does not construct classes.
MissingMetadataError vs InvalidMetadataError
Missing metadata is a class the container was never told about; invalid metadata is a reader answering wrongly.
Normative.
- Only a user-supplied reader is checked — the default decorator reader writes the very metadata it reads back, so there is nothing to check, and a container given no custom reader has nothing to answer for.
- The check runs once per
(reader, class)pair per process, and only over the fields the consumer dereferences (params, and each entry'stoken).- The lifecycle answer also lands in
InvalidMetadataError, with a differentreason: if the reader names apostConstruct/preDestroymethod the instance does not have, it is reported ("lifecycle method 'strat' is not a method on the instance") rather than swallowed. The class name is taken from the instance itself at the throw site, so the happy path carries no extra argument.
A silently skipped hook is a failure the caller cannot see — which is why the lifecycle case is reported rather than swallowed.
AsyncActivationError vs AsyncResolutionError
Both come from the rule in
ActivationHandler and DeactivationHandler — a hook
returning a Promise means the resolve must be resolveAsync(). The difference is where the async source sits:
AsyncResolutionError— the source is a binding's factory, known at the point the binding is selected.AsyncActivationError— the source is a hook, which only reveals itself after the instance has been created. The container cannot know in advance.hookKindsays whether it waspostConstructoronActivation;methodNamepins the exact method when a class has several@postConstruct().
The hook has already run when the error is thrown. A hook cannot be inspected for asyncness before it is called, so when
AsyncActivationErrororAsyncDeactivationErroris raised on a sync lane, the hook body has already executed and returned aPromise. The container adopts that promise (attaching a no-op rejection handler) so a hook that rejects cannot become an unhandled rejection — but any side effect the hook performed has happened. Retry the operation on the async lane (resolveAsync/unbindAsync), which awaits the hook properly.