Skip to content

ALTTechBzh/nestjs-exceptions

Repository files navigation

@altbzh/nestjs-exceptions

Production-ready exception handling for NestJS with multi-context support (HTTP, RPC, WebSocket)

npm version License: MIT TypeScript NestJS

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.

✨ Features

  • 🔒 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

📦 Installation

npm install @altbzh/nestjs-exceptions

Peer Dependencies

Install the required NestJS packages if you haven't already:

npm install @nestjs/common @nestjs/core reflect-metadata rxjs

Optional Dependencies

For RPC/microservices support:

npm install @nestjs/microservices

For WebSocket support:

npm install @nestjs/websockets @nestjs/platform-socket.io

🚀 Quick Start

1. Register the Global Exception Filter

In 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();

2. Throw Exceptions in Your Code

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;
  }
}

3. Standardized Error Response

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"
}

🧩 Core Concepts

Exception Hierarchy

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)

Context Detection

The library automatically detects the execution context:

  1. ExecutionContextStrategy (Highest Priority) - Uses NestJS ArgumentsHost
  2. AsyncStorageStrategy (Medium Priority) - Uses Node.js AsyncLocalStorage
  3. 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
);

Error Codes

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 →

Exception Metadata

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',
});

💻 Usage Examples

HTTP Exception Handling

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;
  }
}

RPC Exception Handling

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) };
  }
}

WebSocket Exception Handling

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 };
  }
}

📚 API Reference

For detailed API documentation, see:

Key Exports

// 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';

⚙️ Configuration

Custom Exception Filter

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);
  }
}

Custom Exception Logger

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

Environment-Specific Configuration

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()),
  );
}

Module Registration

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 {}

🎯 Best Practices

1. Use Specific Exception Classes

// ✅ 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');

2. Provide Meaningful Error Details

// ✅ 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');

3. Use Error Codes for Client-Side Handling

// Backend
throw new UnauthorizedException(ErrorCode.TOKEN_EXPIRED, 'Authentication token has expired', {
  expiresAt: token.expiresAt,
});

// Frontend can handle specifically
if (error.error === 'token_expired') {
  refreshToken();
}

4. Leverage Context Detection

// ✅ 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 },
);

5. Create Domain-Specific Exceptions

// 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(),
    });
  }
}

6. Handle Async Errors Properly

// ✅ 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;
  }
}

📖 Documentation

🤝 Contributing

Contributions are welcome! Please read our Contributing Guide for details on our code of conduct and the process for submitting pull requests.

📝 License

This project is licensed under the MIT License - see the LICENSE file for details.

🔗 Links

💡 Support

If you have any questions or need help, please:

  1. Check the documentation
  2. Search existing issues
  3. Create a new issue if needed

Built with ❤️ for the NestJS community

About

Production-ready exception handling for NestJS with multi-context support (HTTP, RPC, WebSocket)

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published