Repository navigation
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
- Boot: modules and scope
- Warnings that are not errors
- Boot: routes and gateways
- Boot: configuration
- Runtime
- Tests
- Imports and resolution
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.
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.
UsersRepositoryis imported withimport 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.
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.
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.
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.
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.
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.
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() 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.
The container is still resolving at that point. Read it in onBeforeInit() or
onInit(), which run once every provider exists.
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.
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.
The last import wins, so these are two separate instances. Import one, or declare the token locally.
The local binding wins. Remove one, or ignore the warning if the rebinding is what you wanted.
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.
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.
A WebSocket upgrade is a route too, so a gateway and a controller cannot share a path, and neither can two gateways.
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.
A handler decorator on a class with no @Gateway(path).
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.
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.
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.
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 });Use .yml, .yaml, .toml or .json. A YAML file with a --- document
separator parses as an array and is rejected.
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.
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.
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.
Import QueueModule.forRoot() or forRootAsync() from the root module the
worker boots, which is often a different root from the API's.
The [test] block in bunfig.toml. See the first entry on this page.
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.
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.
testClient's JSON helper got a non-JSON response, and the message carries the
status and body it did get.
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.
@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.
Open an issue with the error text, the bunfig.toml, the module that failed and
bun --version:
github.com/petarzarkov/dunx/issues.