From nestjs-clean-arch
Activates when the user asks to throw an exception, handle error, add error handling, throw not found, throw forbidden, throw bad request, or inject exception service in a NestJS Clean Architecture project. Guides correct use of IException interface with proper injection, error codes, and module wiring.
How this skill is triggered — by the user, by Claude, or both
Slash command
/nestjs-clean-arch:exception-handlingThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
The exception system separates the exception contract (domain layer) from the HTTP implementation (infrastructure layer). Use cases throw exceptions through the `IException` interface; the infrastructure layer maps those to HTTP responses.
The exception system separates the exception contract (domain layer) from the HTTP implementation (infrastructure layer). Use cases throw exceptions through the IException interface; the infrastructure layer maps those to HTTP responses.
// src/domain/exceptions/exceptions.interface.ts
export interface IFormatExceptionMessage {
type: string // short error code, e.g. 'TASK_NOT_FOUND'
message: string // human-readable message
}
export const IException = Symbol('IException')
export interface IException {
badRequestException(data: IFormatExceptionMessage): never
internalServerErrorException(data?: IFormatExceptionMessage): never
forbiddenException(data?: IFormatExceptionMessage): never
unauthorizedException(data?: IFormatExceptionMessage): never
notFoundException(data?: IFormatExceptionMessage): never
}
The concrete implementation lives at src/infrastructure/exceptions/exceptions.service.ts and is exported by ExceptionsModule (src/infrastructure/exceptions/exceptions.module.ts).
| Method | HTTP Status | When to Use |
|---|---|---|
notFoundException | 404 | Resource not found by id or lookup key |
badRequestException | 400 | Invalid input, constraint violation, business rule failure |
forbiddenException | 403 | Authenticated but not permitted to perform the action |
unauthorizedException | 401 | No valid authentication (missing or invalid JWT) |
internalServerErrorException | 500 | Unexpected server-side failures |
Import both the interface and the Symbol token. Inject via @Inject(IException) — never instantiate ExceptionsService directly.
import { Injectable, Inject } from '@nestjs/common'
import { ITaskRepository } from '@domain/repositories/task.repository.interface'
import { IException } from '@domain/exceptions/exceptions.interface'
@Injectable()
export class DeleteTaskUseCase {
constructor(
@Inject(ITaskRepository) private readonly taskRepository: ITaskRepository,
@Inject(IException) private readonly exception: IException,
) {}
async execute({ id, userId }: { id: number; userId: number }): Promise<void> {
const task = await this.taskRepository.findOneTask({ id, userId })
if (!task) {
this.exception.notFoundException({
type: 'TASK_NOT_FOUND',
message: `Task with id ${id} not found`,
})
}
await this.taskRepository.deleteTask(id)
}
}
Key rules:
this.exception.<method>(...) directly — no throw keyword required; the return type is never.type as an ALL_CAPS snake_case error code scoped to the domain (e.g. TASK_NOT_FOUND, INVALID_STATUS, USER_ALREADY_EXISTS).message for a human-readable explanation that can be shown to API consumers.Import ExceptionsModule in the feature module so that IException is resolvable in the DI container.
// src/modules/task.module.ts
import { Module } from '@nestjs/common'
import { TypeOrmModule } from '@nestjs/typeorm'
import { Task } from '@infrastructure/databases/postgresql/entities/task.entity'
import { ITaskRepository } from '@domain/repositories/task.repository.interface'
import { IException } from '@domain/exceptions/exceptions.interface'
import { TaskRepository } from '@infrastructure/databases/postgresql/repositories/task.repository'
import { ExceptionsModule } from '@infrastructure/exceptions/exceptions.module'
import { ExceptionsService } from '@infrastructure/exceptions/exceptions.service'
import { DeleteTaskUseCase } from '@use-cases/tasks/delete-task.use-case'
@Module({
imports: [
TypeOrmModule.forFeature([Task]),
ExceptionsModule, // ← registers and exports ExceptionsService
],
providers: [
{ provide: ITaskRepository, useClass: TaskRepository },
{ provide: IException, useClass: ExceptionsService }, // ← bind Symbol → implementation
DeleteTaskUseCase,
],
})
export class TaskModule {}
ExceptionsModule exports ExceptionsService, so importing the module is sufficient. The explicit provider binding ({ provide: IException, useClass: ExceptionsService }) makes the Symbol token resolvable for @Inject(IException) in use cases.
Chain additional guards using the same injected service:
async execute(dto: CreateTaskDto & { userId: number }): Promise<Task> {
if (!dto.title?.trim()) {
this.exception.badRequestException({
type: 'INVALID_TASK_TITLE',
message: 'Task title must not be empty',
})
}
const existing = await this.taskRepository.findTaskByTitle(dto.title, dto.userId)
if (existing) {
this.exception.badRequestException({
type: 'TASK_TITLE_DUPLICATE',
message: `A task with title "${dto.title}" already exists`,
})
}
return this.taskRepository.createTask({ ...dto })
}
Provide a partial mock with only the methods called by the use case under test:
import { IException } from '@domain/exceptions/exceptions.interface'
const mockException = {
notFoundException: jest.fn(),
badRequestException: jest.fn(),
forbiddenException: jest.fn(),
unauthorizedException: jest.fn(),
internalServerErrorException: jest.fn(),
}
const module = await Test.createTestingModule({
providers: [
DeleteTaskUseCase,
{ provide: ITaskRepository, useValue: mockTaskRepository },
{ provide: IException, useValue: mockException },
],
}).compile()
Assert that the correct method was called with the expected payload:
it('throws notFoundException when task does not exist', async () => {
mockTaskRepository.findOneTask.mockResolvedValue(null)
await useCase.execute({ id: 1, userId: 42 })
expect(mockException.notFoundException).toHaveBeenCalledWith({
type: 'TASK_NOT_FOUND',
message: 'Task with id 1 not found',
})
})
| Pattern | Example |
|---|---|
<ENTITY>_NOT_FOUND | TASK_NOT_FOUND, USER_NOT_FOUND |
<ENTITY>_<FIELD>_<REASON> | TASK_TITLE_DUPLICATE, USER_EMAIL_INVALID |
INVALID_<FIELD> | INVALID_STATUS, INVALID_DATE_RANGE |
<ENTITY>_<ACTION>_FORBIDDEN | TASK_DELETE_FORBIDDEN |
Keep error codes scoped to the domain, not to HTTP verbs or implementation details.
IException Symbol from @domain/exceptions/exceptions.interface@Inject(IException) private readonly exception: IExceptionthis.exception.<method>({ type, message }) inside the use case execute() methodExceptionsModule in the feature module's imports array{ provide: IException, useClass: ExceptionsService } in the feature module's providerstype error codes{ provide: IException, useValue: { notFoundException: jest.fn(), ... } } in testsnpx claudepluginhub tuannguyen151/foxdemon-plugins --plugin nestjs-clean-archHandles global error responses in NestJS using @Catch, ExceptionFilter, and custom exception hierarchies. Maps third-party errors (e.g., Prisma) to HTTP exceptions and logs with context.
Implements standardized API error handling with RFC 7807 responses, typed error classes, middleware, and monitoring. Use for consistent HTTP errors across endpoints.
Standardizes error handling across frontend and backend layers with exception hierarchy, error categories, response formats, and boundary patterns.