# @webxsid/nest-exception

![Coverage](https://codecov.io/gh/webxsid/nest-exception/branch/main/graph/badge.svg)
[![Test & Coverage](https://github.com/webxsid/nest-exception/actions/workflows/test-covergare.yml/badge.svg)](https://github.com/webxsid/nest-exception/actions/workflows/test-covergare.yml)
[![NPM Version](https://img.shields.io/npm/v/@webxsid/nest-exception)](https://www.npmjs.com/package/@webxsid/nest-exception)
[![License](https://img.shields.io/github/license/webxsid/nest-exception)](LICENSE)

A centralized exception handling module for NestJS applications. It provides structured error management, logging, and automatic exception handling.

## Features

- **Centralized Error Registry**: Define and manage application errors easily.
- **Automatic Error Handling**: Custom `AppException` class integrates seamlessly.
- **Flexible Error Registration**: Predefine errors in the module or register dynamically.
- **Extendable Error Handling**: Customize error handling with `ExceptionFilter`.
- **Stack Trace (Development Mode)**: Automatically captures stack trace for debugging.
- **Seamless Integration**: Just import the module and start using it.

## Installation

Install the package using npm or yarn:

```bash
$ npm install @webxsid/nest-exception
# or
$ yarn add @webxsid/nest-exception
```

## Usage

### Importing and Setting Up the Module

- Import the `AppExceptionModule` in the root module using `forRoot` or `forRootAsync`:

```typescript
import { Module } from '@nestjs/common';
import { AppExceptionModule } from '@webxsid/nest-exception';

@Module({
    imports: [AppExceptionModule.forRoot({
        isDev: process.env.NODE_ENV === 'development',
        errors: [
            { code: 'E001', message: 'User not found' },
            { code: 'E002', message: 'Invalid credentials' },
        ],
        logger: LoggerService // Any implementation of LoggerService
    })],
})
export class AppModule {}
```

#### Async Configuration

```typescript
import { Module } from '@nestjs/common';
import { AppExceptionModule } from '@webxsid/nest-exception';
import { ConfigModule, ConfigService } from '@nestjs/config';

@Module({
    imports: [
        ConfigModule.forRoot(),
        AppExceptionModule.forRootAsync({
            imports: [ConfigModule],
            useFactory: (configService: ConfigService) => ({
                isDev: configService.get('NODE_ENV') === 'development',
                errors: [
                    { code: 'E001', message: 'User not found' },
                    { code: 'E002', message: 'Invalid credentials' },
                ],
                logger: LoggerService // Any implementation of LoggerService
            }),
            inject: [ConfigService]
        })
    ],
})
export class AppModule {}
```

### Registering the Global Exception Filter

To apply the `AppExceptionFilter` globally in your application, register it in your root module (`AppModule`):

```typescript
// app.module.ts
import { Module } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';
import { AppExceptionFilter } from '@webxsid/nest-exception';

@Module({
    providers: [
        {
            provide: APP_FILTER,
            useClass: AppExceptionFilter,
        },
    ],
})
export class AppModule {}
```

## Error Management

### Registering Errors in the Module

Errors can be pre-registered in the module configuration:

```typescript
imports: [
    AppExceptionModule.forRoot({
        errors: [
            { code: 'E001', message: 'User not found' },
            { code: 'E002', message: 'Invalid credentials' },
        ]
    })
]
```

### Registering Errors Dynamically

- Use the `ExceptionRegistry` service to register errors at runtime:

```typescript
import { Injectable } from '@nestjs/common';
import { ExceptionRegistry } from '@webxsid/nest-exception';

@Injectable()
export class AppService {
    constructor(private readonly exceptionRegistry: ExceptionRegistry) {}

    registerErrors() {
        this.exceptionRegistry.registerError({ code: 'E003', message: 'Invalid request' });
    }
}
```

### Extending the Exception Handler

The `ExceptionHandlerService` allows you to customize how specific exceptions are handled.
It supports both **constructor-based** and **predicate-based** handlers to reliably handle
errors from first-party code as well as third-party libraries (e.g. Mongo / Mongoose).

---

#### Constructor-based handler

```ts
import { Injectable, OnModuleInit, HttpStatus, ArgumentsHost } from '@nestjs/common';
import { ExceptionHandlerService } from '@webxsid/nest-exception';
import { MongoError } from 'mongodb';

@Injectable()
export class MongoErrorHandler implements OnModuleInit {
  constructor(
    private readonly exceptionHandlerService: ExceptionHandlerService,
  ) {}

  onModuleInit() {
    this.exceptionHandlerService.register(
      MongoError,
      (exception: MongoError, host: ArgumentsHost) => {
        const response = host.switchToHttp().getResponse();
        response.status(HttpStatus.BAD_REQUEST).json({
          statusCode: HttpStatus.BAD_REQUEST,
          errorCode: 'MONGO_ERROR',
          message: exception.message,
          timestamp: new Date().toISOString(),
        });
      },
    );
  }
}
```


---

#### Predicate-based handler (recommended for Mongo / Mongoose)
Some libraries throw plain objects instead of real Error instances.
In these cases, constructor matching will not work.

Use registerWhen to match errors by shape or properties.
```ts

import { Injectable, OnModuleInit, HttpStatus, ArgumentsHost } from '@nestjs/common';
import { ExceptionHandlerService } from '@webxsid/nest-exception';

@Injectable()
export class MongoErrorHandler implements OnModuleInit {
  constructor(
    private readonly exceptionHandlerService: ExceptionHandlerService,
  ) {}

  onModuleInit() {
    this.exceptionHandlerService.registerWhen(
      (error: any) =>
        error?.name === 'MongoServerError' ||
        error?.code === 11000,
      (exception, host: ArgumentsHost) => {
        const response = host.switchToHttp().getResponse();
        response.status(HttpStatus.BAD_REQUEST).json({
          statusCode: HttpStatus.BAD_REQUEST,
          errorCode: 'MONGO_ERROR',
          message: exception.message ?? 'MongoDB error',
          timestamp: new Date().toISOString(),
        });
      },
    );
  }
}
```

#### Handler resolution order

When an exception is caught, handlers are resolved in the following order:
	1.	Constructor-based handlers (including inheritance via the prototype chain)
	2.	Predicate-based handlers (canActivate fallback)
	3.	Global exception filter handling (default behavior)

This ensures correct polymorphic behavior while remaining resilient to wrapped or
serialized errors.


#### Registering the handler

Add the handler class to your module providers:


```typescript
@Module({
    imports: [AppExceptionModule.forRoot(/*...*/)],
    providers: [MongoErrorHandler]
})
export class AppModule {}
```

#### Best practices
  - Use constructor-based handlers for application-defined exceptions
  - Use predicate-based handlers for third-party libraries
  - Keep predicates specific to avoid accidental catch-all handlers
  - Avoid relying solely on error names when possible


### Throwing Custom Exceptions

- Use the `AppException` class to throw predefined errors:

```typescript
import { Injectable } from '@nestjs/common';
import { AppException } from '@webxsid/nest-exception';

@Injectable()
export class AppService {
    async getUser(id: string) {
        const user = await this.userService.findById(id);
        if (!user) {
            throw new AppException('E001');
        }
        return user;
    }
}
```

## How It Works

The AppException class simplifies error handling by checking if the provided error code exists in the Exception Registry. Here’s how it behaves in different scenarios:

### 1. ✅ Passing a Registered Error Code

If the error code exists in the registry (either pre-registered in the module or added dynamically), AppException will:
•	Retrieve the corresponding error message and status code.
•	Construct a structured HTTP response with the correct status, message, and code.

```typescript
throw new AppException('E001'); 
```

**Output:**
```json
{
    "statusCode": 400,
    "errorCode": "E001",
    "message": "User not found",
    "timestamp": "2021-09-01T12:00:00.000Z"
}
```
_(Assuming the error code 'E001' is registered with the message 'User not found' and status code 400)_

### 2. ❌ Passing an Unregistered Error Code or String

If the error code is not found in the registry, AppException will:
•	Throw an internal server error with the default message and status code.
•	Log the error using the provided logger service.

```typescript
throw new AppException('Something went wrong'); 
```

**Output:**
```json
{
    "statusCode": 500,
    "errorCode": "UNKNOWN_ERROR",
    "message": "Internal server error",
    "timestamp": "2021-09-01T12:00:00.000Z"
}
```

#### 🛠️ Development Mode (Stack Trace)

If **development mode** (isDev: true) is enabled, AppException will also include a stack trace for easier debugging:

```json
{
    "statusCode": 500,
    "errorCode": "UNKNOWN_ERROR",
    "message": "Internal server error",
    "timestamp": "2021-09-01T12:00:00.000Z",
    "stack": "Error: Internal server error\n    at AppService.getUser (/app/app.service.ts:12:13)\n    at processTicksAndRejections (internal/process/task_queues.js:93:5)"
}
```


## License

This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.

## Acknowledgements

- [NestJS](https://nestjs.com/)
- [TypeScript](https://www.typescriptlang.org/)
- [Jest](https://jestjs.io/)
- [ESLint](https://eslint.org/)

## Author

[Siddharth Mittal](https://webxsid.com/)
