After upgrading nestjs-cls to version 6.0.1, I started encountering test failures in parts of my codebase that depend heavily on the @UseCls() decorator—especially in places where execution occurs outside the typical request lifecycle, like background jobs.
This happens because there’s no HTTP request triggering the CLS context—something @UseCls() expects by default. Even replacing the decorator with an explicit this.cls.run(...) block didn’t solve the problem.
Test code
import { createMock } from '@golevelup/ts-jest';
import { BaseJobRunner } from './base-job-runner.service';
import { JobMetrics } from '@shared/apm/metrics';
import { CustomClsModule } from '@cls';
import {
ClsPluginTransactional,
NoOpTransactionalAdapter,
} from '@nestjs-cls/transactional';
import { Test } from '@nestjs/testing';
describe('BaseJobRunnerService', () => {
class FakeJobRunner extends BaseJobRunner {
async execute(fail: boolean) {
if (fail) throw new Error('test');
}
}
async function buildRunner() {
const app = await Test.createTestingModule({
imports: [
CustomClsModule.register({
clsTransaction: new ClsPluginTransactional({
adapter: new NoOpTransactionalAdapter({
tx: createMock(),
}),
}),
}),
],
providers: [FakeJobRunner],
}).compile();
return app.get(FakeJobRunner);
}
it('should report as nok if the job fails', async () => {
await buildRunner().then((jobRunner) => jobRunner.run(true));
// assert
});
});
CustomClsModule
export class CustomClsModule {
static register(
options: { clsTransaction?: ClsPluginTransactional } = {},
): Nest.DynamicModule {
const clsTransaction =
typeof options.clsTransaction === 'undefined'
? new ClsPluginTransactional({
imports: [TypeOrmModule],
adapter: new TransactionalAdapterTypeOrm({
dataSourceToken: getDataSourceToken(),
}),
enableTransactionProxy: true,
})
: options.clsTransaction;
return {
module: CustomClsModule,
global: true,
imports: [
ClsModule.forRoot({
global: true,
middleware: {
mount: true,
setup: (cls: CustomClsService, request: Request) => {
const storeId = Number(request.get('x-store-id'));
if (!isNaN(storeId)) cls.set('storeId', storeId);
cls.set('reqId', randomUUID());
},
},
plugins: [clsTransaction],
}),
],
providers: [
{
provide: CustomClsService,
useExisting: ClsService,
},
],
exports: [CustomClsService, ClsLogger],
};
}
}
BaseJobRunner class
export abstract class BaseJobRunner<T extends any[] = any[]> {
protected readonly logger: Logger;
constructor(
private readonly name: string,
protected readonly cls: CustomClsService,
) {
this.logger = new Logger(name);
}
@UseCls()
public async run(...args: T): Promise<void> {
await this.cls.run(async () => {
const startDate = new Date();
const start = startDate.getTime();
let result: 'ok' | 'nok' = 'ok';
this.logger.log(
new Log({
message: `Starting job ${this.name}`,
data: { startDate },
}),
);
try {
await this.execute(...args);
} catch (error: any) {
result = 'nok';
this.logger.error(
new ErrorLog({ error, message: `error executing job ${this.name}` }),
);
} finally {
const endDate = new Date();
const elapsed = endDate.getTime() - start;
// report result to metrics
this.logger.log(
new Log({
message: `Finishing job ${this.name}`,
data: { startDate, endDate },
}),
);
}
});
}
protected abstract execute(...args: T): Promise<void>;
}
Error trace
● BaseJobRunnerService › should report as ok if the job succeeds
ClsPluginsHooksHost not initialized
29 | it('should report as ok if the job succeeds', async () => {
30 | const jobRunner = new FakeJobRunner('test', createMock<ClsService>());
> 31 | await jobRunner.run(false);
| ^
32 | ...
33 |
34 |
at Function.getInstance (../node_modules/nestjs-cls/dist/src/lib/plugin/cls-plugin-hooks-host.js:27:19)
at Object.apply (../node_modules/nestjs-cls/dist/src/lib/cls-initializers/use-cls.decorator.js:20:81)
at Object.<anonymous> (shared/cron-job/base-job-runner.service.spec.ts:31:21)
After upgrading
nestjs-clsto version 6.0.1, I started encountering test failures in parts of my codebase that depend heavily on the@UseCls()decorator—especially in places where execution occurs outside the typical request lifecycle, like background jobs.This happens because there’s no HTTP request triggering the CLS context—something
@UseCls()expects by default. Even replacing the decorator with an explicitthis.cls.run(...)block didn’t solve the problem.Test code
CustomClsModule
BaseJobRunner class
Error trace