Skip to content

Boot Errors

Every boot-time validation failure carries a stable MJS#### code. Boot fails fast by design — each code below explains what the loader found and how to fix it. Agents can query these via the MCP server's explain_error tool.

MJS0001 — Circular module dependency

The module graph contains a cycle (A → B → A); the error message traces the exact path. There is no valid load order for a cycle, so the boot refuses instead of picking an arbitrary winner.

Fix: break the cycle. Usually one of the two modules only needs a type from the other (use a type-only import, which creates no edge), or the shared surface belongs in a third module both can import.

MJS0002 — Import target is not a module

A class listed in imports: [...] has no @Module() decorator.

Fix: import the package's module class (e.g. CacheModule), not a service or entity class. Check the symbol you imported from the package barrel.

MJS0003 — Module imports an unregistered dependency

A module's imports: [...] references a module class that was never passed to createApp({ modules: [...] }) (directly or transitively).

Fix: add the missing module to the app's modules array — or, if using @modularityjs/autoload, re-run modularityjs-autodiscover so the generated wiring shell includes it.

MJS0004 — Unfulfilled contract

A contract (abstract class declared via contracts: [...]) has no provider bound at boot — typically the contract module is wired but no driver is.

Fix: add a driver module for the contract to the app's modules array (e.g. CacheModule needs CacheMemoryModule or CacheRedisModule). pnpm modularity add <driver> installs and wires it in one step.

MJS0005 — Duplicate provider registration

Two modules both list the same class in providers: [...]. Each class provider must have exactly one owning module per slot.

Fix: keep the provider in the module that owns it; if the second module wanted to replace an implementation, use preferences: [{ provide, useClass }] instead of re-providing.

MJS0006 — retired

Module-level version constraints were removed: the framework releases in lockstep (Changesets fixed mode), so every @modularityjs/* module is the same version by construction and the check could never fire. @Module() no longer accepts version, and imports no longer accept { module, version }.

This code is retired rather than reused — a new boot validation error takes the next free code, never MJS0006.

MJS0007 — Duplicate pool key

Two modules contribute an entry under the same key to the same pool. Pool keys are unique per pool so contributions stay addressable and removable.

Fix: to genuinely add a second entry, pick a different key. To replace an upstream entry, remove-then-recontribute: { pool, key, remove: true } followed by { pool, key, useClass: Replacement }.

MJS0008 — Pool entries share a class

Multiple entries in the same pool use the same class, which would resolve to one singleton for every key.

Fix: give each pool key its own class (subclass or separate implementation), or contribute a factory entry producing distinct instances.

MJS0009 — Contract declared twice

Two modules both declare the same contract in contracts: [...]. Each contract has exactly one owner — the package that defines the abstraction.

Fix: remove the declaration from the non-owning module. Drivers and extensions reference the contract via imports + preferences/pools, never by re-declaring it.

MJS0010 — Contract used without importing its owner

A module consumes a contract (injects it or contributes to its pool) but does not list the owning module in imports: [...].

Fix: add the contract's module to the consumer's imports. The dependency must be declared so the loader can order initialization correctly.

MJS0011 — Pool contribution without a declared pool

A module contributes to a pool whose token no registered module declares as a contract.

Fix: wire the module that owns the pool (the contract package's module) into the app; contributions cannot exist without an owner to consume them.

MJS0012 — Factory provider injects an unregistered token

A useFactory provider's inject: [...] list references a token no module binds.

Fix: register the missing dependency (add its module or provider), or remove the token from the inject list if the factory no longer needs it.

MJS0013 — Factory pool entry injects an unregistered token

Same as MJS0012, but for a factory-form pool contribution.

Fix: as above — every token in the entry's inject list must be bound by some registered module.

MJS0014 — Class dereferenced before initialization (ESM import cycle)

Unlike the codes above, this one is not thrown by the loader — the crash happens at entrypoint import time, before createApp runs, as a raw Node ReferenceError: Cannot access 'X' before initialization. The modularity runner (dev, start, verify) recognizes the pattern and reports it under this code.

The cause is an ESM import cycle crossed by a decorator argument. Feature barrels widen file-level imports into feature-level runtime edges; when two features import each other's barrels, ESM evaluates one of them partially, and any @Inject(X), imports: [XModule], or useClass: X that dereferences a class from the not-yet-evaluated half hits its temporal dead zone.

Fix: make the runtime feature graph a DAG. Convert cross-feature imports used only as types to import type (erased at runtime — they cannot form cycles); move genuinely shared runtime helpers into a module both features can import. The strict lint preset (createStrictConfig, on in scaffolded apps) enforces this: @typescript-eslint/consistent-type-imports auto-fixes type-only edges and import-x/no-cycle fails lint on any remaining runtime cycle, so the crash is caught at lint time instead of boot.

MJS0015 — Config validation failed at boot

A *Config provider's validate() threw while the app was booting — either while applying forRoot({...}) overrides or during the loader's config sweep, which validates every config class (including bare-imported modules with no forRoot) before any lifecycle hook runs. The code prefixes whatever the config class threw; the exception type and the original message (after the colon) are the config's own. Boot-check mode (pnpm modularity verify) skips this validation by design — config values may come from runtime secrets that assembly checks must not require.

Fix: the wrapped message names the config class, its module, and the offending field. Correct the forRoot({...}) override or the environment value feeding it (for env-driven configs, pnpm modularity cli config:print shows the effective value and winning source per path).

MJS0016 — Decorator used without its source module wired

A method decorator that rides the synthetic-interceptor seam but cannot constructor-inject (@CacheResponse from http-cache, @Idempotent from http-idempotency) reads its services from a module-scoped holder that the owning module's afterLoad populates. The decorator is used but that module was never wired, so the holder is empty. Unlike the loader codes above, this StateException surfaces at first use — the first decorated request — not during boot.

Fix: add the module named in the message to the app's modules array, along with the drivers it needs (HttpCacheModule plus a cache driver; HttpIdempotencyModule plus cache and lock drivers). The decorator alone only registers metadata — the module supplies the services behind it.

MJS0017 — Unfulfilled late contract

A token declared in a module's lateContracts: [...] was still unbound after every module's afterLoad hook had run.

lateContracts exists for the one class of binding MJS0004 structurally cannot see: a contract whose implementation a framework-tier driver constructs from runtime config and binds with a manual container.bind() during afterLoad. At MJS0004 time — before any lifecycle hook — that binding does not exist yet, so the token cannot be listed in contracts:. Declaring it as a late contract instead runs the same fulfilment check a second time, immediately after the afterLoad phase and before onInit opens any socket or pool.

In practice this means the contract module was wired without a driver: DatabaseModule declares DatabaseConnection, TransactionRunner, MigrationRunner, and MigrationGenerator as late contracts, and all four are bound by DatabaseTypeormModule / DatabasePrismaModule in afterLoad.

Fix: add the driver module to the app's modules array (pnpm modularity add @modularityjs/database-typeorm installs and wires it in one step). If you are authoring a contract package, declare lateContracts only for manually-bound tokens — anything bound by a normal { provide, useClass } preference belongs in contracts:, which fails earlier.

MJS0018 — Module registered twice

The same module class was registered more than once under the same slot (the default slot, or the same Named() slot). Only one registration can win, and the loser's configuration — a differing forRoot({...}), configureModule overrides, or activations — would be silently discarded.

Fix: remove the duplicate entry from the app's modules array. To run two configured instances of the same module side by side, register the second under a different slot with Named('<slot>', ...).

MJS0019 — onReady module not covered by any profile

The app defines profiles, but a module implementing onReady belongs to none of them. start('<profile>') only runs the onReady hooks of that profile's modules, so the uncovered module's listener or consumer would silently never start — an outage that looks like a clean boot.

Fix: add the module to the appropriate profile(s) in createApp({ profiles }). If the module should start in every profile, list it in each one. If profiles were not intended, remove the profiles option — without profiles, every onReady runs.

MJS0020 — Pool removal matches no entry

A { pool, key, remove: true } entry targets a key that no earlier module contributed to that pool (in the same slot). A removal that matches nothing is a typo'd key, or the contributing module loading after the remover.

Fix: check the key for a typo against the contributing module's pool entry. If the key is right, add the contributing module to the remover's imports: — imports also order the contribution before the removal.

MJS0021 — Preference for an undeclared token

A module declares a preference for a class token that no registered module declares as a contract (contracts: / lateContracts:), provides, or consumes as an optional seam (@InjectOptional). Such a binding is invisible to every fulfilment check — the preference twin of MJS0011.

Fix: declare the class in the owning module: contracts: for a preference-bound abstract, lateContracts: for a token bound manually in afterLoad. If the preference targets an existing implementation, make sure the class is the exact one the providing package exports (a duplicate import of the same class from two paths creates two distinct tokens). Symbol/string tokens are exempt from this check.

MJS0022 — Conflicting overrides from two unordered modules

Two different modules override the same key — the same scoped preference, constructor argument index, or field — of the same target class (and slot), and neither module transitively imports the other. The loader fixes no order between such a pair, so "last wins" would be a coin flip between deployments.

When one contributor does transitively import the other, this is not an error: the loader sorts the importer last and its override wins, exactly as preferences already behave over the same sorted order. That is what makes the common consumer shape legal — an AppModule importing a package's tweak module and overriding the same field.

Fix: keep exactly one override for that key, move both decisions into the single module that owns them, or give the module that should win an imports: entry for the other so the order is declared rather than accidental. Distinct keys on the same target (one module overrides args[0], another fields.logger) still merge fine.