Back to Scaling Node.js Applications guides

NestJS Error Handling Patterns

Stanley Ulili
Updated on March 14, 2025

When your application encounters an unexpected failure—whether due to a third-party service timeout or a malformed request slipping past validation—it often results in frustrated users, frantic debugging, and potential business losses.

To prevent these disruptions, you should implement effective error-handling strategies that anticipate failures, manage them gracefully, and keep your application stable under pressure.

This article explores NestJS error-handling patterns you can use to build resilient applications, minimize downtime, and improve overall system reliability.

What are errors in NestJS?

When your NestJS application encounters an error, understanding its type is crucial. Different errors require different handling strategies, and recognizing these categories is the first step toward building a system that can recover gracefully and remain reliable under pressure.

Operational errors

These are the everyday hiccups your application must handle gracefully.

For example, a customer tries to access their order history, but their authentication token expired 30 seconds ago. Your application shouldn't crash—it should guide them back to login. That's handling an operational error.

Other real-world examples include:

  • A user attempts to access a deleted account (404 Not Found)
  • A customer submits payment with an expired credit card (400 Bad Request)
  • Your third-party email service goes down during a critical notification (503 Service Unavailable)
  • A client's API token lacks permissions for the requested resource (403 Forbidden)

Programmer errors

Unlike operational errors, these stem directly from code issues. They represent mistakes that shouldn't exist in production and signal the need for fixes rather than runtime accommodations.

The errors often include:

  • Type mismatches that TypeScript should have caught but didn't due to improper typing
  • Circular dependency injections causing your NestJS container to enter infinite loops
  • Forgetting to apply @Injectable() decorators to providers
  • Using async/await incorrectly, causing promise chains to break

System errors

System errors occur when the underlying infrastructure supporting your application fails. These issues arise at the boundary between your code and its execution environment, often beyond direct control.

Critical examples include:

  • Your database connection pool suddenly exhausts during peak traffic
  • The filesystem where you store uploaded files runs out of space
  • Memory consumption spikes unexpectedly, triggering container limits
  • Network partitions isolate your services from communicating properly

With this understanding of what can go wrong, you must build defenses that address each error category differently. A one-size-fits-all approach won't cut it—you need targeted strategies that turn potential disasters into controlled situations.

Leveraging NestJS exception filters

NestJS provides a powerful feature called Exception Filters that centralizes error handling. Exception filters catch exceptions thrown from your controllers, pipes, guards, and interceptors, allowing you to process errors consistently.

The framework includes a built-in HttpException class for HTTP errors, which should be used for operational errors:

 
@Get('users/:id')
findOne(@Param('id') id: string) {
  const user = this.usersService.findOne(id);
  if (!user) {
    throw new HttpException('User not found', HttpStatus.NOT_FOUND);
  }
  return user;
}

For more specific HTTP errors, NestJS provides built-in exceptions like NotFoundException and BadRequestException:

 
@Get('users/:id')
findOne(@Param('id') id: string) {
  const user = this.usersService.findOne(id);
  if (!user) {
    throw new NotFoundException(`User with ID ${id} not found`);
  }
  return user;
}

While these built-in exceptions cover many scenarios, creating a global exception filter provides more control over error responses:

 
@Catch()
export class GlobalExceptionFilter implements ExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();

    const status = 
      exception instanceof HttpException
        ? exception.getStatus()
        : HttpStatus.INTERNAL_SERVER_ERROR;

    const message = 
      exception instanceof HttpException
        ? exception.getResponse()
        : 'Internal server error';

    response.status(status).json({
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: request.url,
      message,
    });
  }
}

Register this filter globally in your main.ts file:

 
import { GlobalExceptionFilter } from './filters/global-exception.filter';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalFilters(new GlobalExceptionFilter());
  await app.listen(3000);
}
bootstrap();

This ensures all unhandled exceptions are captured and formatted consistently, enhancing error visibility while protecting sensitive information.

Creating custom exception classes

NestJS applications often need specialized error handling beyond the built-in exceptions. Custom exception classes provide structured error responses for different application domains.

Start by creating a base application exception:

 
export class AppException extends HttpException {
  constructor(
    message: string,
    statusCode: HttpStatus,
    public readonly errorCode?: string,
    public readonly details?: any,
  ) {
    super(
      {
        message,
        errorCode,
        details,
        timestamp: new Date().toISOString(),
      },
      statusCode,
    );
  }
}

Then extend this base class for domain-specific errors:

 
export class ValidationException extends AppException {
  constructor(message: string, details?: any) {
    super(message, HttpStatus.BAD_REQUEST, 'VALIDATION_ERROR', details);
  }
}

export class ResourceNotFoundException extends AppException {
  constructor(resourceType: string, identifier: string | number) {
    super(
      `${resourceType} with identifier ${identifier} not found`,
      HttpStatus.NOT_FOUND,
      'RESOURCE_NOT_FOUND',
    );
  }
}

export class AuthenticationException extends AppException {
  constructor(message = 'Authentication failed') {
    super(message, HttpStatus.UNAUTHORIZED, 'AUTHENTICATION_FAILED');
  }
}

These specialized exceptions provide context-rich error responses:

 
@Get('products/:id')
async findProduct(@Param('id') id: string) {
  const product = await this.productsService.findOne(id);
  if (!product) {
    throw new ResourceNotFoundException('Product', id);
  }
  return product;
}

The resulting error response includes more meaningful information:

 
{
  "message": "Product with identifier abc123 not found",
  "errorCode": "RESOURCE_NOT_FOUND",
  "timestamp": "2023-05-16T14:30:45.123Z",
  "statusCode": 404
}

This structured approach improves client-side error handling and debugging while maintaining a consistent response format.

Handling asynchronous errors in NestJS

NestJS heavily utilizes async/await patterns, which require special attention for error handling. Fortunately, NestJS automatically catches and processes exceptions from async methods in controllers.

Here's how NestJS handles asynchronous errors internally:

 
@Get('users')
async findAll() {
  // If this throws an error, NestJS will catch and process it
  return await this.usersService.findAll();
}

However, when working with promises directly, you should implement proper error handling:

 
@Get('data')
async fetchExternalData() {
  try {
    const response = await this.httpService.get('https://api.example.com/data').toPromise();
    return response.data;
  } catch (error) {
    if (error.response) {
      // External API returned an error
      throw new ServiceUnavailableException('External service error');
    }
    if (error.request) {
      // Request was made but no response received
      throw new GatewayTimeoutException('External service timeout');
    }
    // Something else caused the error
    throw new InternalServerErrorException('Failed to process request');
  }
}

For operations running outside the request context (like scheduled tasks or event subscribers), use try/catch blocks and proper logging:

 
@Injectable()
export class DataSyncService {
  constructor(private readonly logger: Logger) {}

  @Cron('0 */2 * * * *')
  async syncData() {
    try {
      // Synchronization logic
      await this.dataService.performSync();
      this.logger.log('Data synchronization completed');
    } catch (error) {
      this.logger.error(
        `Data synchronization failed: ${error.message}`,
        error.stack,
      );
      // Optional: Notify monitoring systems
    }
  }
}

This approach ensures that async errors are properly contained and don't crash your application.

Implementing timeout handling with RxJS

NestJS integrates well with RxJS, which provides powerful tools for handling timeouts in HTTP requests and other async operations. Using timeouts prevents operations from hanging indefinitely.

Here's how to implement timeout handling with the HttpService:

 
@Injectable()
export class ExternalApiService {
  constructor(private httpService: HttpService) {}

  async fetchData(): Promise<any> {
    try {
      const response = await this.httpService
        .get('https://api.example.com/data')
        .pipe(
          timeout(5000), // 5 second timeout
          catchError(err => {
            if (err instanceof TimeoutError) {
              throw new RequestTimeoutException('External API request timed out');
            }
            throw new ServiceUnavailableException('External API error');
          }),
        )
        .toPromise();

      return response.data;
    } catch (error) {
      // Re-throw the transformed errors from our pipe
      throw error;
    }
  }
}

For more advanced scenarios, implement retry logic with exponential backoff:

 
async fetchWithRetry(): Promise<any> {
  return this.httpService
    .get('https://api.example.com/data')
    .pipe(
      timeout(3000),
      retry({
        count: 3,
        delay: (error, retryCount) => {
          // Exponential backoff: 1s, 2s, 4s
          return timer(Math.pow(2, retryCount - 1) * 1000);
        },
      }),
      catchError(err => {
        if (err instanceof TimeoutError) {
          throw new RequestTimeoutException('Request timed out after retries');
        }
        throw new ServiceUnavailableException('Service unavailable after retries');
      }),
    )
    .toPromise();
}

This pattern ensures your application remains responsive even when external dependencies fail or become slow.

Implementing rate limiting

Preventing system overload is a critical aspect of error prevention. NestJS can leverage @nestjs/throttler to implement rate limiting at both the application and route levels.

First, install the package:

 
npm install --save @nestjs/throttler

Then configure it in your application module:

 
import { ThrottlerModule } from '@nestjs/throttler';

@Module({
  imports: [
    ThrottlerModule.forRoot({
      ttl: 60,           // time-to-live in seconds
      limit: 10,         // max number of requests within TTL
    }),
  ],
})
export class AppModule {}

Apply rate limiting to specific controllers:

 
import { ThrottlerGuard } from '@nestjs/throttler';

@Controller('auth')
@UseGuards(ThrottlerGuard)
export class AuthController {
  // Routes here will be rate-limited
}

You can also customize rate limits for specific routes:

 
import { Throttle } from '@nestjs/throttler';

@Controller('auth')
export class AuthController {
  // Override global settings for this route
  @Throttle(3, 60) // 3 requests per 60 seconds
  @Post('login')
  async login(@Body() credentials: LoginDto) {
    // Login logic
  }
}

For more sophisticated rate limiting, consider implementing IP-based or user-based throttling through a custom guard:

 
@Injectable()
export class CustomThrottlerGuard extends ThrottlerGuard {
  protected getTracker(req: Record<string, any>): string {
    // Use user ID if authenticated, otherwise IP
    return req.user ? `user-${req.user.id}` : req.ip;
  }
}

Proper rate limiting prevents denial-of-service scenarios and ensures your application remains available for all users.

Final thoughts

Error handling is fundamental to reliable NestJS applications, not just a last-minute safeguard. The discussed patterns help turn system failures into controlled events, ensuring stability and user confidence.

While the official NestJS documentation provides foundational concepts on exception filters and error handling, implementation requires thoughtful integration within your specific architecture.

Treating errors as expected parts of your application flow allows you to create systems that bend rather than break under pressure—the hallmark of production-grade software.

Author's avatar
Article by
Stanley Ulili
Stanley Ulili is a technical educator at Better Stack based in Malawi. He specializes in backend development and has freelanced for platforms like DigitalOcean, LogRocket, and AppSignal. Stanley is passionate about making complex topics accessible to developers.
Got an article suggestion? Let us know
Next article
Exploring Temporal API: The Future of Date Handling in JavaScript
Discover the Temporal API, JavaScript’s modern solution for handling dates and times. Say goodbye to Date quirks like mutability, time zone issues, and inconsistent parsing.
Licensed under CC-BY-NC-SA

This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.

Make your mark

Join the writer's program

Are you a developer and love writing and sharing your knowledge with the world? Join our guest writing program and get paid for writing amazing technical guides. We'll get them to the right readers that will appreciate them.

Write for us
Writer of the month
Marin Bezhanov
Marin is a software engineer and architect with a broad range of experience working...
Build on top of Better Stack

Write a script, app or project on top of Better Stack and share it with the world. Make a public repository and share it with us at our email.

community@betterstack.com

or submit a pull request and help us build better products for everyone.

See the full list of amazing projects on github