How It Works Features Pricing Blog Error Guides
Log In Start Free Trial
NestJS · TypeScript/React

Fix Error: Cannot read properties of undefined (reading 'pipe') in NestJS

This error occurs in a NestJS interceptor when the route handler does not return an Observable, typically because it returns void or a raw value instead. Interceptors expect next.handle() to return an Observable. Fix it by ensuring all intercepted handlers return a value and wrapping non-Observable returns with of().

Reading the Stack Trace

TypeError: Cannot read properties of undefined (reading 'pipe') at LoggingInterceptor.intercept (src/interceptors/logging.interceptor.ts:18:35) at InterceptorsConsumer.intercept (node_modules/@nestjs/core/interceptors/interceptors-consumer.js:22:36) at /node_modules/@nestjs/core/router/router-execution-context.js:46:28 at /node_modules/@nestjs/core/router/router-proxy.js:9:23 at Layer.handle [as handle_request] (node_modules/express/lib/router/layer.js:95:5) at next (node_modules/express/lib/router/route.js:144:13) at Route.dispatch (node_modules/express/lib/router/route.js:114:3) at Layer.handle [as handle_request] (node_modules/express/lib/router/layer.js:95:5) at /node_modules/express/lib/router/index.js:284:15 at Function.process_params (node_modules/express/lib/router/index.js:346:12)

Here's what each line means:

Common Causes

1. Handler returns void

The controller method does not return a value, so next.handle() produces undefined instead of an Observable.

@Post('notify')
async sendNotification(@Body() dto: NotifyDto) {
  await this.notificationService.send(dto);
  // No return value - returns undefined
}

2. Interceptor assumes Observable without checking

The interceptor calls .pipe() on next.handle() without verifying the return type.

intercept(context: ExecutionContext, next: CallHandler) {
  return next.handle().pipe(
    tap(() => console.log('After...')),
  );
  // Crashes if next.handle() returns undefined
}

3. Using @Res() decorator bypasses interceptors

When using @Res() to manually send the response, NestJS skips the standard response pipeline and interceptors receive undefined.

@Get('download')
download(@Res() res: Response) {
  res.sendFile('/path/to/file'); // Bypasses interceptors
}

The Fix

Check whether next.handle() returns a valid Observable before calling .pipe(). If the handler returned void or a non-Observable value, wrap it with of() to create a valid Observable that the interceptor pipeline can process.

Before (broken)
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const now = Date.now();
    return next.handle().pipe(
      tap(() => console.log(`Request took ${Date.now() - now}ms`)),
    );
  }
}
After (fixed)
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable, of } from 'rxjs';
import { tap } from 'rxjs/operators';

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const now = Date.now();
    const result = next.handle();

    if (!result || typeof result.pipe !== 'function') {
      console.log(`Request took ${Date.now() - now}ms`);
      return result ? of(result) : of(undefined);
    }

    return result.pipe(
      tap(() => console.log(`Request took ${Date.now() - now}ms`)),
    );
  }
}

Testing the Fix

import { LoggingInterceptor } from './logging.interceptor';
import { of } from 'rxjs';
import { ExecutionContext, CallHandler } from '@nestjs/common';

describe('LoggingInterceptor', () => {
  let interceptor: LoggingInterceptor;
  const mockContext = {} as ExecutionContext;

  beforeEach(() => {
    interceptor = new LoggingInterceptor();
  });

  it('logs request duration for normal handlers', (done) => {
    const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
    const handler: CallHandler = { handle: () => of({ data: 'test' }) };

    interceptor.intercept(mockContext, handler).subscribe((result) => {
      expect(result).toEqual({ data: 'test' });
      expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Request took'));
      consoleSpy.mockRestore();
      done();
    });
  });

  it('handles void handlers without crashing', (done) => {
    const handler: CallHandler = { handle: () => undefined as any };

    interceptor.intercept(mockContext, handler).subscribe((result) => {
      expect(result).toBeUndefined();
      done();
    });
  });
});

Run your tests:

npm test

Pushing Through CI/CD

git checkout -b fix/nestjs-interceptor-undefined,git add src/interceptors/logging.interceptor.ts src/interceptors/__tests__/logging.interceptor.spec.ts,git commit -m "fix: handle undefined return from next.handle() in interceptor",git push origin fix/nestjs-interceptor-undefined

Your CI config should look something like this:

name: CI
on:
  pull_request:
    branches: [main]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      - run: npm ci
      - run: npm test -- --coverage
      - run: npm run build

The Full Manual Process: 18 Steps

Here's every step you just went through to fix this one bug:

  1. Notice the error alert or see it in your monitoring tool
  2. Open the error dashboard and read the stack trace
  3. Identify the file and line number from the stack trace
  4. Open your IDE and navigate to the file
  5. Read the surrounding code to understand context
  6. Reproduce the error locally
  7. Identify the root cause
  8. Write the fix
  9. Run the test suite locally
  10. Fix any failing tests
  11. Write new tests covering the edge case
  12. Run the full test suite again
  13. Create a new git branch
  14. Commit and push your changes
  15. Open a pull request
  16. Wait for code review
  17. Merge and deploy to production
  18. Monitor production to confirm the error is resolved

Total time: 30-60 minutes. For one bug.

Or Let bugstack Fix It in Under 2 minutes

Every step above? bugstack does it automatically.

Step 1: Install the SDK

npm install bugstack-sdk

Step 2: Initialize

import { initBugStack } from 'bugstack-sdk'

initBugStack({ apiKey: process.env.BUGSTACK_API_KEY })

Step 3: There is no step 3.

bugstack handles everything from here:

  1. Captures the stack trace and request context
  2. Pulls the relevant source files from your GitHub repo
  3. Analyzes the error and understands the code context
  4. Generates a minimal, verified fix
  5. Runs your existing test suite
  6. Pushes through your CI/CD pipeline
  7. Deploys to production (or opens a PR for review)

Time from error to fix deployed: Under 2 minutes.

Human involvement: zero.

Try bugstack Free →

No credit card. 5-minute setup. Cancel anytime.

Deploying the Fix (Manual Path)

  1. Update interceptors to handle undefined returns from next.handle().
  2. Ensure all intercepted handlers return a value.
  3. Avoid using @Res() in routes with interceptors.
  4. Run tests to verify interceptor behavior.
  5. Open a PR, merge after CI, and verify in staging.

Frequently Asked Questions

BugStack runs the fix through your existing test suite, generates additional edge-case tests, and validates that no other modules are affected before marking it safe to deploy.

BugStack never pushes directly to production. Every fix goes through a pull request with full CI checks, so your team can review it before merging.

Only use @Res() for streaming responses or custom headers. For standard JSON responses, return the value and let NestJS handle serialization so interceptors and exception filters work correctly.

Yes, interceptors execute in order of registration. Global interceptors run first, then controller-level, then method-level. Each wraps the next like middleware.