Skip to content

Troubleshooting

petarzarkov edited this page Sep 13, 2026 · 1 revision

Troubleshooting

dunx fails at boot rather than at the first request, and the messages name the class, the parameter and the module involved. Find the phrase you are looking at below.

Boot: dependency injection

no dependencies were recorded for it, so @dunx/transform did not transform

UsersService declares 1 constructor parameter(s) but no dependencies were
recorded for it, so @dunx/transform did not transform UsersService.

The preload never ran. The container spots this by comparing ctor.length against the record it expected to find, so the message arrives instead of an undefined three frames from the mistake.

Put both entries in bunfig.toml:

preload = ["@dunx/transform/preload"]

[test]
preload = ["@dunx/transform/preload"]

Bun's test runner reads its own preload key, so an app that boots fine can still fail on the first bun test. If the message arrives only under bun test, the [test] block is what is missing.

The same error has a second form, when the entrypoint ends in .js, .cjs or .mjs:

The entrypoint is ./dist/main.js, so this is a prebuilt tree: the preload plugin
only matches .ts, and no preload setting can change that. Record the
dependencies at build time instead:

  import { depsPlugin } from '@dunx/transform';
  await Bun.build({ /* ... */ plugins: [depsPlugin] });

No bunfig.toml change fixes that one. Add depsPlugin to the build.

names nothing that exists at runtime, so there is no token to resolve

UsersService cannot be constructed: parameter 1 (UsersRepository) names nothing
that exists at runtime, so there is no token to resolve.

The parameter's type erased. An interface, a primitive, a union and a type-only import all compile away, leaving the container with nothing to look up. The remedy comes attached to the message in one of two forms:

Replace the type with an abstract class, or bind it with token() and declare the parameter as that token.

UsersRepository is imported with import type, which erases it. Make it a value import.

The second is the common one, and editors add import type on their own. An interface at an injection site is always a boot error: the container resolves runtime values, so annotate with a class or an abstract class.

Cannot resolve X in module "Y"

Three endings, each naming a different fix.

Nothing binds it anywhere.

Cannot resolve UsersRepository for UsersService in module "UsersModule".
Nothing in the module graph declares it. Bind it with provide() in a module's
providers, and export it if the consumer is in a different module.

A module binds it but keeps it private. Absent exports means nothing is exported.

... "DataModule" declares it and "UsersModule" imports that module, but it does
not export UsersRepository. Add UsersRepository to that module's exports, or
move the provider into "UsersModule".

A module binds and exports it, but the consumer never imports that module.

... "DataModule" declares it, but "UsersModule" does not import it. Add that
module to "UsersModule"'s imports, or give it global: true.

The container is scoped rather than flat, so a provider is reachable only through a module that exports it to a module that imports it. Background in Architecture: dependency injection.

A missing useFactory dependency reports the same way with no for <consumer> suffix. A dynamic module is its own scope, so a factory injecting a provider needs that provider's module in the imports of the forRootAsync config. Every forRootAsync takes an AsyncModuleConfig, which has imports; a bare FactoryProvider does not.

Circular dependency: A -> B -> A

The dependency record is a thunk evaluated at resolution, so an ordinary circular import resolves on its own and needs no forwardRef. This message means a genuine cycle in the object graph: A cannot be built until B exists and the reverse. Break it by moving the shared piece into a third provider, or by having one side depend on an event rather than on the other class.

app.get() cannot say which you mean

UsersRepository is declared by "DataModule" and "AdminModule", so app.get()
cannot say which you mean. Resolve it from a provider in the module you want, or
export exactly one of them.

Two scopes hold their own instance and app.get() sits outside both.

Boot: modules and scope

is not a dunx module

AppModule is not a dunx module. Decorate it with @Module({ providers: [...] }),
or import a configured one from a static factory such as AppModule.forRoot().

Usually a missing @Module(), or a forRoot() referenced without being called.

Duplicate binding for X

The same token appears twice in one providers list. A DynamicModule's own binding replaces the one its class's @Module decorator declares, so this is two entries on the same side rather than a decorator and a forRoot() disagreeing.

exports X, but does not declare it

Module "UsersModule" exports UsersRepository, but does not declare it and no
module it imports exports it. Add it to this module's providers, or import the
module that provides it.

A module can re-export only what it can reach.

inject(X) was called outside of construction

inject() works in a field initializer or a constructor of a class the container builds, because that is what decides which module scope resolves the token. A call from a static initializer, a module-level statement or a plain new has no scope to read.

AppRef was read during construction

The container is still resolving at that point. Read it in onBeforeInit() or onInit(), which run once every provider exists.

Warnings that are not errors

These land in app.warnings and let the app start. Each one means two instances exist where one was intended, so they are worth reading.

is registered N times, and each registration binds ... again

A configured module is keyed on the object forRoot() returned, and forRoot() returns a new object per call, so two calls build two scopes holding separate instances. Call it once and share the result, or mark the module global: true.

A module that takes no options should be a decorated class rather than a forRoot(), for the same reason.

imports X from both "A" and "B"

The last import wins, so these are two separate instances. Import one, or declare the token locally.

declares X, which module "B" also exports to it

The local binding wins. Remove one, or ignore the warning if the rebinding is what you wanted.

A framework service that self-binds

An unbound class self-binds into whichever scope asks for it first, so a second consumer in another scope becomes a boot error. Bind framework services from a module rather than leaving them to self-bind.

Boot: routes and gateways

Route collision

Route collision: GET /users is declared by UsersController.list and by
AdminController.list. Bun would keep only one of them.

Two handlers claim one method and path. RPC collision is the same check over Connect services, and Path collision is a middleware path a route already claimed, where the route wins so the middleware would never see it.

Gateway path collision

A WebSocket upgrade is a route too, so a gateway and a controller cannot share a path, and neither can two gateways.

is registered as a controller but declares no routes

Add a @Get/@Post/... method, or move the class to providers. The gateway form asks for an @OnMessage/@OnOpen/... method or for the @Gateway decorator to go.

is a websocket handler, but ... is not a gateway

A handler decorator on a class with no @Gateway(path).

must be called before listen()

The route table and the middleware chain fold into one closure per route when the server binds, so a later call could not take effect.

X is a contract, not an implementation

CacheStore, ThrottleStore, RetryClassifier and WsRelay are abstract contracts. Each message names both the module option that binds a real one and the in-process default you get by leaving the option out.

Boot: configuration

Configuration is invalid

The validate function rejected the environment. The detail lists each issue as path: message. ConfigModule.forRoot({ validate }) takes one validation function over Bun.env and any YAML, TOML or JSON you loaded.

Bun reads .env itself, so a missing value usually means the variable is absent, not that a loader failed to run.

Config key "x" is not set

Read through a declared subclass and hand it to as, which is what keeps the type through a factory's inject:

export class AppConfigService extends ConfigService<AppConfig> {}
ConfigModule.forRoot({ validate, as: AppConfigService });

Config file "x" has no parser / did not parse / must hold an object

Use .yml, .yaml, .toml or .json. A YAML file with a --- document separator parses as an array and is rejected.

Runtime

ClientAddress has no server yet / PubSub has no server yet

Both read the live Bun server, so they are available once listen() has run. Move the call into a request path or a lifecycle hook that runs after boot.

Invalid body / Invalid query (400)

Request validation failed, and the issues ride on the error. Unsupported content type is a 415 and lists what a declared body accepts. Malformed application/json body is a 400 for a body that never parsed.

No handler for job / No handler consumes

A worker with nothing to do would idle forever, so an unconsumed queue is a boot error rather than a silent no-op. The message lists the queues that do have handlers. Decorate a method with @JobHandler or @AmqpHandler and declare its class in a module the worker's root imports.

A worker needs the queue bindings, and this module graph has none

Import QueueModule.forRoot() or forRootAsync() from the root module the worker boots, which is often a different root from the API's.

Tests

Providers resolve in the app but not under bun test

The [test] block in bunfig.toml. See the first entry on this page.

Nothing to override for X

An override replaces a binding and cannot add one. If no module binds the token and it is not a class, there is nothing under test that would resolve it.

createTestServer: ... implement Middleware ... but no middleware was supplied

A warning. The fixture is not the application: no global guard runs, and onError is the default mapper. Bind the same HttpOptionsProvider the application binds, or pass middleware: [] to say the omission was meant.

That is not JSON - use request()

testClient's JSON helper got a non-JSON response, and the message carries the status and body it did get.

Imports and resolution

An extensionless relative import fails to resolve

There is no dunx error for this one. It surfaces as Bun's own resolver error, because the scaffold sets moduleResolution: nodenext, where a relative specifier carries its extension. Write ./users.service.js even though the file is .ts.

could not parse

@dunx/transform could not parse a source file. The message names the file and the parse detail. Syntax the Bun runtime accepts and oxc-parser does not is worth an issue.

Still stuck

Open an issue with the error text, the bunfig.toml, the module that failed and bun --version: github.com/petarzarkov/dunx/issues.

Clone this wiki locally