Fix: NestJS Interceptor Not Triggered — Interceptors Not Running
Part of: JavaScript & TypeScript Errors
Quick Answer
How to fix NestJS interceptors not being called — global vs controller vs method binding, response transformation, async interceptors, execution context, and interceptor ordering.
The Problem
A NestJS interceptor is registered but never executes:
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
console.log('Interceptor called'); // This never prints
return next.handle();
}
}
// Registered globally in main.ts:
app.useGlobalInterceptors(new LoggingInterceptor());
// But the log never appearsOr the interceptor runs but the response transformation doesn’t apply:
@Injectable()
export class TransformInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
map(data => ({ success: true, data })), // Response should be wrapped
);
}
}
// Response is still the raw data — not wrapped in { success: true, data: ... }Or the interceptor runs for REST routes but not for GraphQL or WebSocket:
// Interceptor works on GET /users but not on the GraphQL resolver
@Query(() => [User])
async users() {
return this.usersService.findAll();
}
// Interceptor not triggered for GraphQL queriesWhy This Happens
NestJS interceptors sit in the request/response pipeline, but several configuration issues prevent them from running:
- Binding level mismatch —
useGlobalInterceptors()inmain.tsdoesn’t have access to the DI container (for injecting services). UseAPP_INTERCEPTORprovider instead for interceptors that need injection. - Exception thrown before interceptor — if a Guard rejects the request, interceptors after it in the pipeline don’t run. Guards run before interceptors.
@UseInterceptors()applied to wrong level — a method-level decorator doesn’t affect other methods in the controller, and a class-level decorator doesn’t affect parent class methods.- Not handling the Observable correctly —
intercept()must return an Observable. If you return a Promise or a plain value, NestJS may not process the response correctly. - GraphQL/WebSocket context — interceptors for HTTP don’t automatically apply to GraphQL resolvers or WebSocket gateways without context switching.
- Execution order — multiple interceptors run in registration order. If one interceptor throws or short-circuits, later interceptors don’t run.
The NestJS request pipeline runs in a strict order: Middleware -> Guards -> Interceptors (before) -> Pipes -> Route Handler -> Interceptors (after) -> Exception Filters. Understanding this order is critical because a failure at any earlier stage prevents later stages from executing. If your interceptor never runs, the request is being rejected before it reaches the interceptor stage — most commonly by a Guard returning false or throwing an UnauthorizedException.
A less obvious cause: when you register an interceptor globally via app.useGlobalInterceptors(new MyInterceptor()), you’re creating the interceptor outside the NestJS dependency injection container. The interceptor works fine as long as it has no constructor dependencies. The moment you add constructor(private readonly logger: Logger), the dependency is undefined at runtime — not an error that crashes the app, but the interceptor silently misbehaves because this.logger is undefined.
Diagnostic Timeline
When your interceptor is registered but not firing, walk through the NestJS pipeline from front to back.
Minute 0 — Confirm the request reaches the server. Add a temporary console.log directly in the route handler:
@Get('users')
getUsers() {
console.log('Route handler reached'); // Does this print?
return this.usersService.findAll();
}If the handler doesn’t print, the request is being blocked before the route — check middleware, guards, or the route path itself. If it prints, the request does reach the handler, and the issue is with how the interceptor is registered or implemented.
Minute 1 — Check the pipeline order. Add temporary logging to each layer to see what fires:
// Middleware
export class LogMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
console.log('[Middleware] fired');
next();
}
}
// Guard
@Injectable()
export class LogGuard implements CanActivate {
canActivate(context: ExecutionContext) {
console.log('[Guard] fired');
return true;
}
}
// Interceptor
@Injectable()
export class LogInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler) {
console.log('[Interceptor] fired');
return next.handle();
}
}If [Middleware] and [Guard] print but [Interceptor] does not, a guard between them is blocking. If only [Middleware] prints, the guard is rejecting the request.
Minute 2 — Check for a guard that short-circuits. If you use @UseGuards(AuthGuard) on the controller or globally, the guard’s canActivate runs before any interceptor. If canActivate returns false or throws, the interceptor never runs. Temporarily remove the guard to confirm.
Minute 3 — Check the registration method. If you used app.useGlobalInterceptors(new MyInterceptor()) in main.ts, verify the interceptor class has no constructor parameters. If it does, switch to APP_INTERCEPTOR:
// Check: does the interceptor constructor need injected dependencies?
@Injectable()
export class MyInterceptor implements NestInterceptor {
constructor(private readonly logger: Logger) {} // Needs DI
// ...
}
// If yes, useGlobalInterceptors(new MyInterceptor()) silently breaks.
// Switch to APP_INTERCEPTOR in a module.Minute 4 — Check scope alignment. If you used @UseInterceptors(MyInterceptor) on a specific controller, verify the request is actually hitting that controller and not a different one with the same route prefix. NestJS routes can overlap. Add console.log(context.getClass().name) inside the interceptor to confirm which controller the request matched.
Minute 5 — Verify the Observable chain is correct. If the interceptor runs (you see the “before” log) but the response transformation doesn’t apply, check that intercept() returns the Observable from next.handle().pipe(...) — not just next.handle() without the pipe, and not a new Observable that ignores the handler’s response.
Fix 1: Register Global Interceptors with APP_INTERCEPTOR
useGlobalInterceptors() in main.ts creates the interceptor outside the DI container — it can’t inject services. Use APP_INTERCEPTOR for interceptors that need dependencies:
// WRONG — can't inject services, runs outside DI
// main.ts
app.useGlobalInterceptors(new LoggingInterceptor());
// If LoggingInterceptor needs LogService injected, this fails
// CORRECT — use APP_INTERCEPTOR in a module
// app.module.ts
import { APP_INTERCEPTOR } from '@nestjs/core';
import { LoggingInterceptor } from './interceptors/logging.interceptor';
@Module({
providers: [
{
provide: APP_INTERCEPTOR,
useClass: LoggingInterceptor,
},
],
})
export class AppModule {}
// Now LoggingInterceptor can inject services:
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
constructor(private readonly logger: Logger) {} // DI works with APP_INTERCEPTOR
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
this.logger.log('Request received');
return next.handle();
}
}useGlobalInterceptors() is still valid for simple interceptors with no dependencies:
// main.ts — simple interceptors without DI
app.useGlobalInterceptors(
new TimeoutInterceptor(),
new TransformInterceptor(),
);Fix 2: Implement the Interceptor Correctly
The intercept method must return an Observable. Missing the return or mishandling the Observable chain causes the interceptor to silently fail:
// WRONG — missing return of next.handle()
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
console.log('Before...');
next.handle(); // Not returned! Request hangs — no response sent
}
}
// WRONG — returns a Promise instead of Observable
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
async intercept(context: ExecutionContext, next: CallHandler): Promise<any> {
// ^^^^ async makes this return a Promise — breaks Observable pipeline
console.log('Before...');
return next.handle(); // next.handle() returns Observable, wrapped in Promise
}
}// CORRECT — return Observable from next.handle()
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap, map, catchError } from 'rxjs/operators';
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const start = Date.now();
const req = context.switchToHttp().getRequest();
console.log(`→ ${req.method} ${req.url}`);
return next.handle().pipe(
tap(() => {
console.log(`← ${req.method} ${req.url} (${Date.now() - start}ms)`);
}),
);
}
}Response transformation interceptor:
@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, { success: boolean; data: T }> {
intercept(
context: ExecutionContext,
next: CallHandler,
): Observable<{ success: boolean; data: T }> {
return next.handle().pipe(
map(data => ({
success: true,
data,
timestamp: new Date().toISOString(),
})),
);
}
}Fix 3: Apply Interceptors at the Right Level
Interceptors can be applied globally, per-controller, or per-method:
// Method level — only this endpoint
@Get('users')
@UseInterceptors(LoggingInterceptor)
getUsers() {
return this.usersService.findAll();
}
// Controller level — all endpoints in this controller
@Controller('users')
@UseInterceptors(LoggingInterceptor, CacheInterceptor)
export class UsersController {
@Get()
findAll() { ... } // Both interceptors run
@Get(':id')
findOne() { ... } // Both interceptors run
}
// Global — all routes
// In main.ts:
app.useGlobalInterceptors(new LoggingInterceptor());
// Or in AppModule with APP_INTERCEPTOR (preferred for DI)@UseInterceptors() with class vs instance:
// Pass the class — NestJS instantiates it (uses DI)
@UseInterceptors(LoggingInterceptor)
// Pass an instance — no DI, you manage it manually
@UseInterceptors(new LoggingInterceptor())For interceptors that need injected services, always pass the class (not an instance) with @UseInterceptors() or use APP_INTERCEPTOR.
Fix 4: Handle Async Operations in Interceptors
If you need async operations before or after the route handler:
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable, from } from 'rxjs';
import { switchMap, mergeMap } from 'rxjs/operators';
@Injectable()
export class AuditInterceptor implements NestInterceptor {
constructor(private readonly auditService: AuditService) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const req = context.switchToHttp().getRequest();
// Async operation BEFORE the route handler — use from() to convert Promise to Observable
return from(this.auditService.logRequest(req)).pipe(
switchMap(() => next.handle()), // Then call the route handler
mergeMap(async (data) => {
// Async operation AFTER the route handler
await this.auditService.logResponse(req, data);
return data; // Return the original response
}),
);
}
}Simpler async pattern with from():
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
// switchMap converts the sync response to an async one
switchMap(async (data) => {
const enriched = await this.enrichData(data);
return enriched;
}),
);
}Fix 5: Handle Exceptions in Interceptors
Interceptors can catch exceptions thrown by route handlers and transform them:
import { throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
@Injectable()
export class ErrorInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
catchError((error) => {
// Transform or log errors
console.error('Error caught by interceptor:', error);
// Re-throw as a different exception
if (error.code === 'ECONNREFUSED') {
return throwError(() => new ServiceUnavailableException('Database unavailable'));
}
// Re-throw the original error
return throwError(() => error);
}),
);
}
}Note: Exception Filters run AFTER interceptors for exceptions. If an interceptor’s
catchErrordoesn’t handle the error, it propagates to the Exception Filter. The execution order is: Middleware -> Guards -> Interceptors (before) -> Route Handler -> Interceptors (after) -> Exception Filters.
Fix 6: Adapt Interceptors for GraphQL Context
HTTP interceptors use context.switchToHttp(). For GraphQL, switch to the GraphQL context:
@Injectable()
export class GraphQLInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const contextType = context.getType<'http' | 'graphql' | 'ws'>();
if (contextType === 'graphql') {
const gqlContext = GqlExecutionContext.create(context);
const { req } = gqlContext.getContext();
console.log('GraphQL operation:', gqlContext.getInfo().fieldName);
} else if (contextType === 'http') {
const req = context.switchToHttp().getRequest();
console.log('HTTP request:', req.url);
}
return next.handle();
}
}Apply to GraphQL resolver:
@Resolver(() => User)
@UseInterceptors(LoggingInterceptor) // Applies to all resolvers in this class
export class UsersResolver {
@Query(() => [User])
@UseInterceptors(CacheInterceptor) // Applies only to this query
async users() {
return this.usersService.findAll();
}
}Fix 7: Debug Interceptor Execution Order
Multiple interceptors run in registration order (outermost first, innermost last for “before”; reverse order for “after”):
// Registration order matters
providers: [
{ provide: APP_INTERCEPTOR, useClass: FirstInterceptor }, // Outermost
{ provide: APP_INTERCEPTOR, useClass: SecondInterceptor }, // Middle
{ provide: APP_INTERCEPTOR, useClass: ThirdInterceptor }, // Innermost
]
// Execution order:
// Request: First → Second → Third → Route Handler
// Response: Third → Second → FirstAdd temporary logging to debug:
@Injectable()
export class DebugInterceptor implements NestInterceptor {
constructor(private readonly name: string) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
console.log(`[${this.name}] before`);
return next.handle().pipe(
tap({
next: (data) => console.log(`[${this.name}] after success:`, data),
error: (err) => console.log(`[${this.name}] after error:`, err.message),
complete: () => console.log(`[${this.name}] complete`),
}),
);
}
}
// Use with explicit name for debugging
app.useGlobalInterceptors(
new DebugInterceptor('Interceptor1'),
new DebugInterceptor('Interceptor2'),
);Check if a Guard is blocking before the interceptor:
// Guards run before interceptors — if a guard rejects, interceptors don't run
@Controller('admin')
@UseGuards(JwtAuthGuard) // Runs first
@UseInterceptors(LoggingInterceptor) // Only runs if guard passes
export class AdminController { ... }If LoggingInterceptor never logs, check whether JwtAuthGuard is rejecting the request.
Still Not Working?
Verify intercept() is implemented — if the class doesn’t implement NestInterceptor properly, NestJS may silently skip it:
// Wrong — missing @Injectable() or wrong method name
export class MyInterceptor {
// Wrong: method named 'handle' instead of 'intercept'
handle(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle();
}
}
// Correct
@Injectable()
export class MyInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle();
}
}Global interceptors from main.ts run after module interceptors — if you register an interceptor in main.ts AND via APP_INTERCEPTOR, the APP_INTERCEPTOR runs first.
Microservice context — in NestJS microservices, interceptors use context.switchToRpc(). HTTP interceptors don’t apply to microservice message handlers without context switching.
Interceptor skipped for SSE (Server-Sent Events) endpoints — routes decorated with @Sse() return an Observable directly, bypassing the normal response pipeline. Interceptors that transform the response via next.handle().pipe(map(...)) don’t work as expected because the response is streamed, not returned as a single value. For SSE endpoints, handle transformations inside the Observable returned by the route handler itself.
Fastify adapter differences — if you use NestJS with Fastify instead of Express, context.switchToHttp().getRequest() returns a Fastify FastifyRequest, not an Express Request. Properties like req.body, req.params, and req.query work the same, but req.ip, req.hostname, and middleware chaining behave differently. If your interceptor works with Express but not Fastify, check property access patterns.
Interceptor runs twice per request — this usually happens when you register the interceptor both via APP_INTERCEPTOR in a module and via @UseInterceptors() on a controller/method. The module-level interceptor applies globally, and the decorator adds it again. Use one method, not both.
For related NestJS issues, see Fix: NestJS Guard Not Working, Fix: NestJS Circular Dependency, Fix: NestJS Module Not Found, and Fix: Jest Mock Not Working.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: NestJS Swagger UI Not Showing — /api-docs Returns 404 or Blank Page
How to fix NestJS Swagger UI not displaying — SwaggerModule setup, DocumentBuilder, decorators not appearing, guards blocking the docs route, and Fastify vs Express differences.
Fix: NestJS Nest can't resolve dependencies — Provider Not Found Error
How to fix NestJS dependency injection errors — module imports, provider exports, circular dependencies, dynamic modules, and the most common 'can't resolve dependencies' patterns.
Fix: NestJS ValidationPipe Not Working — class-validator Decorators Ignored
How to fix NestJS ValidationPipe not validating requests — global pipe setup, class-transformer, whitelist and transform options, custom validators, and DTO inheritance issues.
Fix: NestJS Guard Not Working — canActivate Always Passes or Is Never Called
How to fix NestJS guards not working — applying guards globally vs controller vs method level, JWT AuthGuard, metadata with Reflector, public routes, and guard execution order.