Skip to content
codefastlabs

Command Palette

Search for a command to run...

@codefast/div0.13.0· Specification

Error hierarchy

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 a code string (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, plus ScopeViolationDetails.

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. InternalError means 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's token).
  • The lifecycle answer also lands in InvalidMetadataError, with a different reason: if the reader names a postConstruct/preDestroy method 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. hookKind says whether it was postConstruct or onActivation; methodName pins 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 AsyncActivationError or AsyncDeactivationError is raised on a sync lane, the hook body has already executed and returned a Promise. 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.