Skip to content

Fix: NestJS Interceptor Not Triggered — Interceptors Not Running

FixDevs ·

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 appears

Or 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 queries

Why This Happens

NestJS interceptors sit in the request/response pipeline, but several configuration issues prevent them from running:

  • Binding level mismatchuseGlobalInterceptors() in main.ts doesn’t have access to the DI container (for injecting services). Use APP_INTERCEPTOR provider 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 correctlyintercept() 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 catchError doesn’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 → First

Add 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.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles