Production-ready exception handling for NestJS with multi-context support (HTTP, RPC, WebSocket)
A comprehensive exception handling system for NestJS applications that provides type-safe, context-aware error handling across HTTP, RPC (gRPC/microservices), and WebSocket communication. Built with enterprise applications in mind, featuring automatic context detection, standardized error responses, and structured logging.
- 🔒 Type-Safe Exception Handling - Full TypeScript support with generic error details
- 🌐 Multi-Context Support - Seamless handling across HTTP, RPC, and WebSocket
- 🎯 Automatic Context Detection - Smart detection using multiple strategies
- 📊 Standardized Error Responses - Consistent error format across all contexts
- 🔢 Comprehensive Error Codes - 20+ predefined error codes for common scenarios
- 🔍 Global Exception Filter - Centralized error handling and transformation
- 📝 Structured Logging - Rich request context with every error log
- 🛡️ Environment-Aware - Different error details for development vs production
- ⚡ Zero Dependencies - Only peer dependencies on NestJS packages
npm install @altbzh/nestjs-exceptionsInstall the required NestJS packages if you haven't already:
npm install @nestjs/common @nestjs/core reflect-metadata rxjsFor RPC/microservices support:
npm install @nestjs/microservicesFor WebSocket support:
npm install @nestjs/websockets @nestjs/platform-socket.ioIn your main.ts:
import { NestFactory } from '@nestjs/core';
import { GlobalExceptionFilter } from '@altbzh/nestjs-exceptions';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Register global exception filter
app.useGlobalFilters(app.get(GlobalExceptionFilter));
await app.listen(3000);
}
bootstrap();import { Injectable } from '@nestjs/common';
import { NotFoundException, ErrorCode } from '@altbzh/nestjs-exceptions';
@Injectable()
export class UserService {
async findById(id: number) {
const user = await this.repository.findOne(id);
if (!user) {
throw new NotFoundException(ErrorCode.RESOURCE_NOT_FOUND, `User with ID ${id} not found`, {
resourceType: 'User',
resourceId: id,
});
}
return user;
}
}Your API will automatically return consistent error responses:
{
"error": "resource_not_found",
"message": "User with ID 123 not found",
"details": {
"resourceType": "User",
"resourceId": 123
},
"timestamp": "2024-01-15T10:30:00.000Z"
}The library provides a clear exception hierarchy:
AppException (abstract base)
├── HttpAppException (HTTP/REST APIs)
│ ├── BadRequestException (400)
│ ├── UnauthorizedException (401)
│ ├── ForbiddenException (403)
│ ├── NotFoundException (404)
│ ├── ConflictException (409)
│ └── InternalServerErrorException (500)
├── RpcAppException (Microservices/gRPC)
└── WsAppException (WebSocket)
The library automatically detects the execution context:
- ExecutionContextStrategy (Highest Priority) - Uses NestJS ArgumentsHost
- AsyncStorageStrategy (Medium Priority) - Uses Node.js AsyncLocalStorage
- StackTraceStrategy (Lowest Priority) - Analyzes stack traces (dev only)
Manual override is available when needed:
import { NotFoundException, ContextType } from '@altbzh/nestjs-exceptions';
throw new NotFoundException(
'resource_not_found',
'User not found',
{ userId: 123 },
{ context: ContextType.HTTP }, // Manual override
);Predefined error codes organized by category:
// Authentication & Authorization
ErrorCode.AUTHENTICATION_REQUIRED;
ErrorCode.INVALID_CREDENTIALS;
ErrorCode.TOKEN_EXPIRED;
ErrorCode.INSUFFICIENT_PERMISSIONS;
// Validation
ErrorCode.VALIDATION_FAILED;
ErrorCode.INVALID_INPUT;
// Resources
ErrorCode.RESOURCE_NOT_FOUND;
ErrorCode.RESOURCE_ALREADY_EXISTS;
// Server Errors
ErrorCode.INTERNAL_SERVER_ERROR;
ErrorCode.SERVICE_UNAVAILABLE;See full list of error codes →
All exceptions support typed metadata:
interface ResourceErrorDetails {
resourceType: string;
resourceId: string | number;
action?: 'create' | 'read' | 'update' | 'delete';
}
throw new NotFoundException<ResourceErrorDetails>(ErrorCode.RESOURCE_NOT_FOUND, 'User not found', {
resourceType: 'User',
resourceId: 123,
action: 'read',
});import { Injectable } from '@nestjs/common';
import {
BadRequestException,
NotFoundException,
ConflictException,
ErrorCode,
} from '@altbzh/nestjs-exceptions';
@Injectable()
export class UserService {
async create(email: string, password: string) {
// Validation error
if (!this.isValidEmail(email)) {
throw new BadRequestException(ErrorCode.VALIDATION_FAILED, 'Invalid email format', {
field: 'email',
value: email,
});
}
// Conflict error
const existing = await this.findByEmail(email);
if (existing) {
throw new ConflictException(
ErrorCode.RESOURCE_ALREADY_EXISTS,
'User with this email already exists',
{ email },
);
}
return this.repository.create({ email, password });
}
async findById(id: number) {
const user = await this.repository.findOne(id);
// Not found error
if (!user) {
throw new NotFoundException(ErrorCode.RESOURCE_NOT_FOUND, `User with ID ${id} not found`, {
resourceType: 'User',
resourceId: id,
});
}
return user;
}
}import { Injectable } from '@nestjs/common';
import { RpcAppException, GrpcStatus } from '@altbzh/nestjs-exceptions';
@Injectable()
export class UserRpcService {
async getUser(id: number) {
const user = await this.repository.findOne(id);
if (!user) {
throw new RpcAppException({
errorCode: 'user_not_found',
message: 'User does not exist',
details: { userId: id },
grpcStatus: GrpcStatus.NOT_FOUND, // gRPC status code
});
}
return user;
}
async authenticateUser(credentials: { email: string; password: string }) {
const user = await this.validateCredentials(credentials);
if (!user) {
throw new RpcAppException({
errorCode: 'invalid_credentials',
message: 'Authentication failed',
grpcStatus: GrpcStatus.UNAUTHENTICATED,
});
}
return { token: this.generateToken(user) };
}
}import { WebSocketGateway, SubscribeMessage, MessageBody } from '@nestjs/websockets';
import { WsAppException } from '@altbzh/nestjs-exceptions';
@WebSocketGateway()
export class ChatGateway {
@SubscribeMessage('subscribe')
handleSubscribe(@MessageBody() data: { topicId: string }) {
const topic = this.topics.get(data.topicId);
if (!topic) {
throw new WsAppException({
errorCode: 'invalid_subscription',
message: 'Topic does not exist',
details: { topicId: data.topicId },
wsCode: 1008, // Policy Violation
});
}
return { success: true, topicId: data.topicId };
}
@SubscribeMessage('sendMessage')
handleMessage(@MessageBody() data: { message: string }) {
if (!data.message || data.message.length > 1000) {
throw new WsAppException({
errorCode: 'invalid_message',
message: 'Message must be between 1-1000 characters',
wsCode: 1003, // Unsupported Data
});
}
this.broadcast(data.message);
return { success: true };
}
}For detailed API documentation, see:
- Complete API Reference - Full documentation of all classes and methods
- Usage Examples - More detailed examples and patterns
- Architecture Guide - Technical design and architecture decisions
// Base Classes
import {
AppException,
HttpAppException,
RpcAppException,
WsAppException,
} from '@altbzh/nestjs-exceptions';
// HTTP Exceptions
import {
BadRequestException,
UnauthorizedException,
ForbiddenException,
NotFoundException,
ConflictException,
InternalServerErrorException,
} from '@altbzh/nestjs-exceptions';
// Filters & Services
import {
GlobalExceptionFilter,
ExceptionLogger,
ExceptionTransformer,
} from '@altbzh/nestjs-exceptions';
// Constants & Types
import { ErrorCode, ContextType, GrpcStatus } from '@altbzh/nestjs-exceptions';
// Utilities
import { ContextDetector } from '@altbzh/nestjs-exceptions';You can extend the GlobalExceptionFilter to customize behavior:
import { Injectable, ExecutionContext } from '@nestjs/common';
import { GlobalExceptionFilter } from '@altbzh/nestjs-exceptions';
@Injectable()
export class CustomExceptionFilter extends GlobalExceptionFilter {
// Override to add custom logging
catch(exception: unknown, host: ArgumentsHost): void {
// Add custom logic here
this.customLogger.log('Exception caught', exception);
// Call parent implementation
super.catch(exception, host);
}
}Customize logging behavior by extending ExceptionLogger:
import { Injectable } from '@nestjs/common';
import { ExceptionLogger } from '@altbzh/nestjs-exceptions';
@Injectable()
export class CustomExceptionLogger extends ExceptionLogger {
logException(exception: unknown, context: ExecutionContext): void {
// Add custom logging logic (e.g., send to external service)
this.sendToMonitoringService(exception);
// Call parent implementation
super.logException(exception, context);
}
private sendToMonitoringService(exception: unknown): void {
// Integration with Sentry, DataDog, etc.
}
}Control error detail visibility:
// main.ts
const app = await NestFactory.create(AppModule);
if (process.env.NODE_ENV === 'production') {
// Production: minimal error details
app.useGlobalFilters(
new GlobalExceptionFilter(new ExceptionTransformer(), new ExceptionLogger()),
);
} else {
// Development: detailed error information including stack traces
app.useGlobalFilters(
new GlobalExceptionFilter(new ExceptionTransformer(), new ExceptionLogger()),
);
}Register exception handling services in your module:
import { Module } from '@nestjs/common';
import {
GlobalExceptionFilter,
ExceptionLogger,
ExceptionTransformer,
} from '@altbzh/nestjs-exceptions';
@Module({
providers: [GlobalExceptionFilter, ExceptionLogger, ExceptionTransformer],
exports: [GlobalExceptionFilter],
})
export class ExceptionModule {}// ✅ Good - Specific exception with context
throw new NotFoundException(ErrorCode.RESOURCE_NOT_FOUND, 'User not found', { userId: 123 });
// ❌ Bad - Generic error
throw new Error('User not found');// ✅ Good - Rich context for debugging
throw new BadRequestException(ErrorCode.VALIDATION_FAILED, 'Email validation failed', {
field: 'email',
value: userInput.email,
reason: 'Invalid format',
expectedFormat: 'user@example.com',
});
// ❌ Bad - Minimal context
throw new BadRequestException(ErrorCode.VALIDATION_FAILED, 'Validation failed');// Backend
throw new UnauthorizedException(ErrorCode.TOKEN_EXPIRED, 'Authentication token has expired', {
expiresAt: token.expiresAt,
});
// Frontend can handle specifically
if (error.error === 'token_expired') {
refreshToken();
}// ✅ Good - Let the library detect context automatically
throw new NotFoundException(ErrorCode.RESOURCE_NOT_FOUND, 'User not found');
// ⚠️ Only override when necessary
throw new NotFoundException(
ErrorCode.RESOURCE_NOT_FOUND,
'User not found',
{},
{ context: ContextType.HTTP },
);// Create reusable exceptions for your domain
export class UserNotFoundException extends NotFoundException {
constructor(userId: number) {
super(ErrorCode.RESOURCE_NOT_FOUND, `User with ID ${userId} not found`, {
resourceType: 'User',
resourceId: userId,
action: 'read',
});
}
}
export class InvalidUserCredentialsException extends UnauthorizedException {
constructor(email: string) {
super(ErrorCode.INVALID_CREDENTIALS, 'Invalid email or password', {
email,
loginAttempt: new Date().toISOString(),
});
}
}// ✅ Good - Let errors bubble up
async findUser(id: number) {
const user = await this.repository.findOne(id);
if (!user) {
throw new NotFoundException(/* ... */);
}
return user;
}
// ❌ Bad - Catching and swallowing errors
async findUser(id: number) {
try {
return await this.repository.findOne(id);
} catch (error) {
console.log(error); // Don't do this
return null;
}
}- API Reference - Complete API documentation
- Architecture Guide - Design decisions and architecture
- Usage Examples - Detailed usage patterns
- Migration Guide - Upgrading from previous versions
- Contributing - How to contribute to this project
Contributions are welcome! Please read our Contributing Guide for details on our code of conduct and the process for submitting pull requests.
This project is licensed under the MIT License - see the LICENSE file for details.
If you have any questions or need help, please:
- Check the documentation
- Search existing issues
- Create a new issue if needed
Built with ❤️ for the NestJS community