nest_10_filters - Exception Filters¶
Your API will throw errors The question isn't if but whether those errors come back as consistent JSON or as Express default HTML with stack traces that leak your entire application structure Exception filters catch thrown exceptions and format them into standardized responses - no stack traces to prod , no inconsistent error shapes , no debugging your error handling at 2AM
what's in here¶
- Exception filter interface and @Catch decorator
- Built-in HttpException and Nest's exception hierarchy
- Custom exception classes
- Global , controller , and method-scoped filters
- Consistent error response formatting
- Logging errors without leaking sensitive data
the exception filter interface¶
import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common'
import { Request, Response } from 'express'
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp()
const response = ctx.getResponse<Response>()
const request = ctx.getRequest<Request>()
const status = exception.getStatus()
const exceptionResponse = exception.getResponse()
const errorResponse = {
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
method: request.method,
message: typeof exceptionResponse === 'string'
? exceptionResponse
: (exceptionResponse as any).message || exception.message,
// NEVER include stack trace in production responses
}
// log the error internally
console.error(`[${request.method}] ${request.url} - ${status}: ${JSON.stringify(errorResponse)}`)
response
.status(status)
.json(errorResponse)
}
}
@Catch(HttpException) tells Nest which exception types this filter handles The catch() method receives the exception and the ArgumentsHost (which gives access to request/response) Every response follows the same structure - clients don't need to handle multiple error shapes
built-in HTTP exceptions¶
import {
BadRequestException, // 400
UnauthorizedException, // 401
ForbiddenException, // 403
NotFoundException, // 404
MethodNotAllowedException, // 405
NotAcceptableException, // 406
RequestTimeoutException, // 408
ConflictException, // 409
GoneException, // 410
PayloadTooLargeException, // 413
UnsupportedMediaTypeException, // 415
UnprocessableEntityException, // 422
InternalServerErrorException, // 500
ServiceUnavailableException, // 503
} from '@nestjs/common'
@Post()
create(@Body() dto: CreateUserDto) {
if (emailExists(dto.email)) {
throw new ConflictException('email already registered')
}
if (isRateLimited()) {
throw new TooManyRequestsException() // 429
}
throw new InternalServerErrorException('something broke - we\'re looking into it')
// note: generic message , no internal details leaked
}
Each exception maps to the correct HTTP status code The message is what the client sees - make it useful but don't leak internals Never expose stack traces , file paths , database query details , or dependency versions
custom exception classes¶
import { HttpException, HttpStatus } from '@nestjs/common'
export class BusinessRuleException extends HttpException {
constructor(rule: string, details?: string) {
const response = {
error: 'BUSINESS_RULE_VIOLATION',
rule,
details,
timestamp: new Date().toISOString()
}
super(response, HttpStatus.UNPROCESSABLE_ENTITY)
}
}
// usage
if (user.accountBalance < order.total) {
throw new BusinessRuleException(
'INSUFFICIENT_FUNDS',
`balance ${user.accountBalance} < required ${order.total}`
)
}
Custom exceptions carry application-specific error context The error field lets clients programmatically handle different error types BusinessRuleException returns 422 with structured data instead of a generic 400
catching all exceptions (catch-all filter)¶
import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus } from '@nestjs/common'
import { Request, Response } from 'express'
@Catch() // catches EVERYTHING
export class AllExceptionsFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp()
const response = ctx.getResponse<Response>()
const request = ctx.getRequest<Request>()
let status = HttpStatus.INTERNAL_SERVER_ERROR
let message: any = 'internal server error'
if (exception instanceof HttpException) {
status = exception.getStatus()
const exceptionResponse = exception.getResponse()
message = typeof exceptionResponse === 'string'
? exceptionResponse
: (exceptionResponse as any).message || message
} else if (exception instanceof Error) {
// log the real error with stack trace internally
console.error(`[UNHANDLED] ${request.method} ${request.url}:`, exception.stack)
}
response.status(status).json({
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
message
})
}
}
@Catch() with no arguments catches every exception type For HttpException - format the known response For unknown errors - log the stack trace internally but return only "internal server error" to the client This is your safety net - never let an unhandled error reach Express's default error handler
filter scopes¶
// method-scoped
@Post()
@UseFilters(new HttpExceptionFilter())
create() {}
// controller-scoped
@Controller('users')
@UseFilters(HttpExceptionFilter) // class reference (DI enabled)
export class UsersController {}
// global in main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule)
app.useGlobalFilters(new AllExceptionsFilter())
await app.listen(3000)
}
// global through DI (app.module.ts)
@Module({
providers: [
{
provide: APP_FILTER,
useClass: AllExceptionsFilter
}
]
})
Global filters catch everything - register once and never think about it Controller/method filters catch specific handlers - use for special error handling needs Passing the class reference (not instance) enables DI in the filter
consistent error response format¶
// Standard error response sent to client
{
"statusCode": 422,
"timestamp": "2026-03-15T03:12:44.123Z",
"path": "/api/v1/users",
"message": [
"email must be a valid email address",
"password must be at least 8 characters"
]
}
// Business rule violation
{
"statusCode": 422,
"timestamp": "2026-03-15T03:12:44.123Z",
"path": "/api/v1/orders",
"error": "BUSINESS_RULE_VIOLATION",
"rule": "INSUFFICIENT_FUNDS",
"details": "balance 50 < required 200"
}
// Auth failure
{
"statusCode": 401,
"timestamp": "2026-03-15T03:12:44.123Z",
"path": "/api/v1/admin/users",
"message": "invalid or expired token"
}
Consistent error shape means frontends write one error handler Validation errors have an array of messages (one per failed constraint) Auth errors don't reveal whether the user exists - just "invalid credentials"
security note: what NOT to include in errors¶
BAD - NEVER DO THIS:
{
"statusCode": 500,
"message": "Cannot read property 'id' of undefined",
"stack": "TypeError: Cannot read property 'id' of undefined\n at UsersService.findOne (src/users/users.service.ts:42:17)\n at UsersController.findOne (src/users/users.controller.ts:15:9)",
"query": "SELECT * FROM users WHERE id = '123'"
}
GOOD:
{
"statusCode": 500,
"message": "internal server error"
}
Stack traces leak file paths , line numbers , and sometimes credentials SQL queries in error messages expose your schema If you need more detail in development , use NODE_ENV checks to conditionally include information
prerequisites¶
nest_09_pipes - Pipes & Validation