Fix: NestJS Interceptor Not Triggered — Interceptors Not Running
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.
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.
For related NestJS issues, see Fix: NestJS Guard Not Working and Fix: NestJS Circular Dependency.
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.