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
Here's what each line means:
- at LoggingInterceptor.intercept (src/interceptors/logging.interceptor.ts:18:35): Your interceptor at line 18 calls .pipe() on the result of next.handle(), but it returned undefined because the handler has no return value.
- at InterceptorsConsumer.intercept (node_modules/@nestjs/core/interceptors/interceptors-consumer.js:22:36): NestJS's interceptor consumer runs all registered interceptors around the handler execution.
- at /node_modules/@nestjs/core/router/router-execution-context.js:46:28: The router execution context wraps the handler call and pipes through interceptors before sending the response.
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.
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`)),
);
}
}
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:
- Notice the error alert or see it in your monitoring tool
- Open the error dashboard and read the stack trace
- Identify the file and line number from the stack trace
- Open your IDE and navigate to the file
- Read the surrounding code to understand context
- Reproduce the error locally
- Identify the root cause
- Write the fix
- Run the test suite locally
- Fix any failing tests
- Write new tests covering the edge case
- Run the full test suite again
- Create a new git branch
- Commit and push your changes
- Open a pull request
- Wait for code review
- Merge and deploy to production
- 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:
- Captures the stack trace and request context
- Pulls the relevant source files from your GitHub repo
- Analyzes the error and understands the code context
- Generates a minimal, verified fix
- Runs your existing test suite
- Pushes through your CI/CD pipeline
- 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)
- Update interceptors to handle undefined returns from next.handle().
- Ensure all intercepted handlers return a value.
- Avoid using @Res() in routes with interceptors.
- Run tests to verify interceptor behavior.
- 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.