fix(core): name the module when metadata entries are invalid (#17819)

* fix(core): name the module when metadata entries are invalid

A custom provider without a usable useClass, useValue, useFactory or
useExisting, and an undefined entry in controllers or exports, crashed
bootstrap with a bare TypeError naming neither the module nor the token.

Throw a descriptive error instead, as undefined imports and providers
already do.

* test(core): cover the injectable path and the invalid provider message

Pin the paths the previous commit changed: a custom injectable without a
valid "use*" property, the message text for string, symbol and class
tokens, and null as well as undefined in the two container specs.
This commit is contained in:
Roman Khegay authored and GitHub committed 2026-09-22 15:19:57 +02:00
1 parent a1080277c2
commit fb8e0d6529
7 files changed
+139 -1

No files matched your search

@@ -0,0 +1,9 @@
import { InjectorDependency } from '../../injector/injector.js';
import { INVALID_PROVIDER_MESSAGE } from '../messages.js';
import { RuntimeException } from './runtime.exception.js';
export class InvalidProviderException extends RuntimeException {
constructor(token: InjectorDependency, moduleName: string) {
super(INVALID_PROVIDER_MESSAGE(token, moduleName));
}
}
+10
View File
@@ -224,6 +224,16 @@ Potential causes:
Scope [${stringifyScope(scope)}]`;
};
export const INVALID_PROVIDER_MESSAGE = (
token: InjectorDependency,
moduleName: string,
) => `Nest cannot create the ${moduleName} instance.
The provider ${getDependencyName(token, 'provider')} in the ${moduleName} "providers" array does not define a valid "useClass", "useValue", "useFactory" or "useExisting" property.
Potential causes:
- The value of "useClass", "useFactory" or "useExisting" is undefined at runtime, often because of a circular import between files. Check your import statements.
- None of these properties is set, or the one that is set is null.`;
export const UNKNOWN_EXPORT_MESSAGE = (
token: string | symbol = 'item',
module: string,
+7
View File
@@ -22,6 +22,7 @@ import {
type EnhancerSubtype,
GLOBAL_MODULE_METADATA,
type Injectable,
isNil,
type NestApplicationContextOptions,
} from '@nestjs/common/internal';
import type { Type } from '@nestjs/common';
@@ -291,6 +292,9 @@ export class NestContainer {
throw new UnknownModuleException();
}
const moduleRef = this.modules.get(token)!;
if (isNil(toExport)) {
throw new CircularDependencyException(moduleRef.metatype.name);
}
moduleRef.addExportedProviderOrModule(toExport);
}
@@ -299,6 +303,9 @@ export class NestContainer {
throw new UnknownModuleException();
}
const moduleRef = this.modules.get(token)!;
if (isNil(controller)) {
throw new CircularDependencyException(moduleRef.metatype.name);
}
moduleRef.addController(controller);
const controllerRef = moduleRef.controllers.get(controller)!;
+6
View File
@@ -5,6 +5,7 @@ import {
RuntimeException,
UnknownExportException,
} from '../errors/exceptions/index.js';
import { InvalidProviderException } from '../errors/exceptions/invalid-provider.exception.js';
import { createContextId } from '../helpers/context-id-factory.js';
import { getClassScope } from '../helpers/get-class-scope.js';
import { isDurable } from '../helpers/is-durable.js';
@@ -318,6 +319,11 @@ export class Module {
this.addCustomFactory(provider, collection, enhancerSubtype);
} else if (this.isCustomUseExisting(provider)) {
this.addCustomUseExisting(provider, collection, enhancerSubtype);
} else {
throw new InvalidProviderException(
(provider as ClassProvider).provide,
this.name,
);
}
return provider.provide;
}
@@ -1,6 +1,7 @@
import { UnknownDependenciesException } from '../../../errors/exceptions/unknown-dependencies.exception.js';
import {
INVALID_MODULE_MESSAGE,
INVALID_PROVIDER_MESSAGE,
UNDEFINED_MODULE_MESSAGE,
UNKNOWN_EXPORT_MESSAGE,
USING_INVALID_CLASS_AS_A_MODULE_MESSAGE,
@@ -383,6 +384,42 @@ Scope [AppModule -> CatsModule]`);
});
});
describe('INVALID_PROVIDER_MESSAGE', () => {
const expectedMessage = (token: string) =>
stringCleaner(`Nest cannot create the AppModule instance.
The provider ${token} in the AppModule "providers" array does not define a valid "useClass", "useValue", "useFactory" or "useExisting" property.
Potential causes:
- The value of "useClass", "useFactory" or "useExisting" is undefined at runtime, often because of a circular import between files. Check your import statements.
- None of these properties is set, or the one that is set is null.`);
it('should display a string token', () => {
const actualMessage = stringCleaner(
INVALID_PROVIDER_MESSAGE('CATS_SERVICE', 'AppModule'),
);
expect(actualMessage).toBe(expectedMessage('"CATS_SERVICE"'));
});
it('should display a symbol token', () => {
const actualMessage = stringCleaner(
INVALID_PROVIDER_MESSAGE(Symbol('CATS_SERVICE'), 'AppModule'),
);
expect(actualMessage).toBe(expectedMessage('Symbol(CATS_SERVICE)'));
});
it('should display a class token', () => {
class CatsService {}
const actualMessage = stringCleaner(
INVALID_PROVIDER_MESSAGE(CatsService, 'AppModule'),
);
expect(actualMessage).toBe(expectedMessage('CatsService'));
});
});
describe('INVALID_MODULE_MESSAGE', () => {
it('should display the received `null` value and its type', () => {
const expectedMessage =
@@ -33,6 +33,30 @@ describe('NestContainer', () => {
);
});
it.each([undefined, null])(
'should "addController" throw "CircularDependencyException" naming the module when controller is %s',
async nilValue => {
const { moduleRef } = (await container.addModule(TestModule, []))!;
const addController = () =>
container.addController(nilValue!, moduleRef.token);
expect(addController).toThrow(CircularDependencyException);
expect(addController).toThrow('inside TestModule');
},
);
it.each([undefined, null])(
'should "addExportedProviderOrModule" throw "CircularDependencyException" naming the module when exported item is %s',
async nilValue => {
const { moduleRef } = (await container.addModule(TestModule, []))!;
const addExport = () =>
container.addExportedProviderOrModule(nilValue!, moduleRef.token);
expect(addExport).toThrow(CircularDependencyException);
expect(addExport).toThrow('inside TestModule');
},
);
it('should "addController" throw "UnknownModuleException" when module is not stored in collection', () => {
expect(() => container.addController(null!, 'TestModule')).toThrow(
UnknownModuleException,
+46 -1
View File
@@ -1,6 +1,7 @@
import { Controller, Scope } from '@nestjs/common';
import { Module as ModuleDecorator } from '../../../common/decorators/modules/module.decorator.js';
import { Injectable } from '../../../common/index.js';
import { InvalidProviderException } from '../../errors/exceptions/invalid-provider.exception.js';
import { RuntimeException } from '../../errors/exceptions/runtime.exception.js';
import { UnknownElementException } from '../../errors/exceptions/unknown-element.exception.js';
import { UnknownExportException } from '../../errors/exceptions/unknown-export.exception.js';
@@ -74,9 +75,21 @@ describe('Module', () => {
it('should call `addCustomProvider`', () => {
const addCustomProviderSpy = vi.spyOn(moduleRef, 'addCustomProvider');
moduleRef.addInjectable({ provide: 'test' } as any, 'guard');
moduleRef.addInjectable(
{ provide: 'test', useValue: 'test' } as any,
'guard',
);
expect(addCustomProviderSpy).toHaveBeenCalled();
});
it('should throw when a custom injectable does not define a valid "use*" property', () => {
expect(() =>
moduleRef.addInjectable(
{ provide: 'test', useClass: undefined } as any,
'guard',
),
).toThrow(InvalidProviderException);
});
});
it('should add provider', () => {
@@ -158,6 +171,38 @@ describe('Module', () => {
expect(addCustomUseExisting).toHaveBeenCalled();
});
describe('when custom provider does not define a valid "useClass", "useValue", "useFactory" or "useExisting"', () => {
const expectInvalidProvider = (provider: object) => {
const addCustomProvider = () =>
moduleRef.addCustomProvider(provider as any, new Map());
expect(addCustomProvider).toThrow(InvalidProviderException);
expect(addCustomProvider).toThrow(
'The provider "test" in the TestModule "providers" array',
);
};
it('should throw when "useClass" is undefined', () => {
expectInvalidProvider({ provide: 'test', useClass: undefined });
});
it('should throw when "useClass" is null', () => {
expectInvalidProvider({ provide: 'test', useClass: null });
});
it('should throw when "useExisting" is undefined', () => {
expectInvalidProvider({ provide: 'test', useExisting: undefined });
});
it('should throw when "useFactory" is null', () => {
expectInvalidProvider({ provide: 'test', useFactory: null });
});
it('should throw when none of them is set', () => {
expectInvalidProvider({ provide: 'test' });
});
});
describe('addCustomClass', () => {
const type = { name: 'TypeTest' };
const provider = { provide: type, useClass: type, durable: true };