Logging
Chapter 38: Logging & Error Handling
Section titled “Chapter 38: Logging & Error Handling”Implementing Robust Logging and Error Management
Section titled “Implementing Robust Logging and Error Management”38.1 Logging Overview
Section titled “38.1 Logging Overview”Effective logging is crucial for debugging, monitoring, and auditing applications.
Logging Architecture ================================================================================
Application | v +------------------+ | Logger | <-- Centralized logging +------------------+ | +--------+--------+--------+ | | | | v v v v +-------+ +-------+ +-------+ +-------+ |Console| | File | | Log | | Cloud | | | | | | Service| |Watch | +-------+ +-------+ +-------+ +-------+
Log Levels: - error: Application errors - warn: Warning conditions - info: Informational messages - debug: Debug information - verbose: Detailed tracing
================================================================================38.2 NestJS Logger
Section titled “38.2 NestJS Logger”Basic Logger Setup
Section titled “Basic Logger Setup”import { NestFactory } from '@nestjs/core';import { AppModule } from './app.module';import { Logger } from '@nestjs/common';
async function bootstrap() { const app = await NestFactory.create(AppModule, { logger: ['error', 'warn', 'log', 'debug', 'verbose'], });
const logger = new Logger('Bootstrap'); await app.listen(3000); logger.log(`Application running on port 3000`);}bootstrap();Custom Logger
Section titled “Custom Logger”import { Injectable, LoggerService } from '@nestjs/common';import { ConfigService } from '@nestjs/config';import * as fs from 'fs';import * as path from 'path';
@Injectable()export class CustomLoggerService implements LoggerService { private logLevel: number; private logLevels = ['error', 'warn', 'log', 'debug', 'verbose'];
constructor(private configService: ConfigService) { const level = configService.get('LOG_LEVEL', 'info'); this.logLevel = this.logLevels.indexOf(level); }
error(message: any, trace?: string, context?: string) { this.log('error', message, context, trace); }
warn(message: any, context?: string) { this.log('warn', message, context); }
log(message: any, context?: string) { this.log('log', message, context); }
debug(message: any, context?: string) { this.log('debug', message, context); }
verbose(message: any, context?: string) { this.log('verbose', message, context); }
private log( level: string, message: any, context?: string, trace?: string, ) { const levelIndex = this.logLevels.indexOf(level);
if (levelIndex > this.logLevel) { return; }
const timestamp = new Date().toISOString(); const formattedMessage = this.formatMessage(level, message, context, trace);
// Console output this.logToConsole(level, formattedMessage);
// File output this.logToFile(level, formattedMessage); }
private formatMessage( level: string, message: any, context?: string, trace?: string, ): string { const timestamp = new Date().toISOString(); const contextStr = context ? ` [${context}]` : ''; const traceStr = trace ? `\n${trace}` : '';
return `${timestamp}${contextStr} ${level.toUpperCase()}: ${message}${traceStr}`; }
private logToConsole(level: string, message: string) { switch (level) { case 'error': console.error('\x1b[31m%s\x1b[0m', message); break; case 'warn': console.warn('\x1b[33m%s\x1b[0m', message); break; default: console.log(message); } }
private logToFile(level: string, message: string) { const logDir = path.join(process.cwd(), 'logs');
if (!fs.existsSync(logDir)) { fs.mkdirSync(logDir, { recursive: true }); }
const logFile = path.join(logDir, `${level}.log`); fs.appendFileSync(logFile, message + '\n'); }}
// Register in main.tsconst app = await NestFactory.create(AppModule, { bufferLogs: true,});app.useLogger(new CustomLoggerService(app.get(ConfigService)));38.3 Structured Logging
Section titled “38.3 Structured Logging”JSON Logging
Section titled “JSON Logging”import { Injectable, LoggerService } from '@nestjs/common';import { ConfigService } from '@nestjs/config';
interface LogEntry { timestamp: string; level: string; message: string; context?: string; trace?: string; userId?: string; requestId?: string; metadata?: Record<string, any>;}
@Injectable()export class JsonLoggerService implements LoggerService { constructor(private configService: ConfigService) {}
error(message: any, trace?: string, context?: string) { this.log('error', message, { context, trace }); }
warn(message: any, context?: string) { this.log('warn', message, { context }); }
log(message: any, context?: string) { this.log('info', message, { context }); }
debug(message: any, context?: string) { this.log('debug', message, { context }); }
verbose(message: any, context?: string) { this.log('trace', message, { context }); }
private log( level: string, message: any, options: { context?: string; trace?: string; metadata?: Record<string, any>; } = {}, ) { const entry: LogEntry = { timestamp: new Date().toISOString(), level, message: typeof message === 'string' ? message : JSON.stringify(message), ...options, };
console.log(JSON.stringify(entry)); }}Request Context Logging
Section titled “Request Context Logging”import { Injectable, NestMiddleware } from '@nestjs/common';import { Request, Response, NextFunction } from 'express';import { v4 as uuidv4 } from 'uuid';import { Logger } from '@nestjs/common';
@Injectable()export class RequestContextMiddleware implements NestMiddleware { private logger = new Logger('Request');
use(req: Request, res: Response, next: NextFunction) { const requestId = uuidv4(); const startTime = Date.now();
// Attach context to request (req as any).context = { requestId, userId: (req as any).user?.id, };
// Log request this.logger.log({ message: 'Incoming request', requestId, method: req.method, url: req.url, userAgent: req.headers['user-agent'], ip: req.ip, });
// Log response on finish res.on('finish', () => { const duration = Date.now() - startTime;
this.logger.log({ message: 'Request completed', requestId, method: req.method, url: req.url, statusCode: res.statusCode, duration: `${duration}ms`, }); });
next(); }}38.4 TypeORM Query Logging
Section titled “38.4 TypeORM Query Logging”Query Logger
Section titled “Query Logger”import { Logger } from '@nestjs/common';import { QueryRunner } from 'typeorm';
export class QueryLogger implements Logger { private logger = new Logger('Database');
log(level: 'log' | 'info' | 'warn', message: any) { this.logger[level](message); }
logMigration(message: string) { this.logger.log(message); }
logQuery(query: string, parameters?: any[]) { this.logger.debug({ query, parameters: parameters?.length ? parameters : undefined, }); }
logQueryError(error: string, query: string, parameters?: any[]) { this.logger.error({ error, query, parameters: parameters?.length ? parameters : undefined, }); }
logQuerySlow(time: number, query: string, parameters?: any[]) { this.logger.warn({ message: 'Slow query detected', duration: `${time}ms`, query, parameters: parameters?.length ? parameters : undefined, }); }
logSchemaBuild(message: string) { this.logger.log(message); }}
// Use in database configexport default new DataSource({ type: 'postgres', // ... connection settings logger: new QueryLogger(), maxQueryExecutionTime: 1000, // Log slow queries});38.5 Error Handling
Section titled “38.5 Error Handling”Global Exception Filter
Section titled “Global Exception Filter”import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus, Logger,} from '@nestjs/common';import { Request, Response } from 'express';import { QueryFailedError } from 'typeorm';
interface ErrorResponse { statusCode: number; message: string; error: string; timestamp: string; path: string; requestId?: string; details?: any;}
@Catch()export class AllExceptionsFilter implements ExceptionFilter { private logger = new Logger('Exception');
catch(exception: unknown, host: ArgumentsHost) { const ctx = host.switchToHttp(); const response = ctx.getResponse<Response>(); const request = ctx.getRequest<Request>();
let status: number; let message: string; let error: string; let details: any;
if (exception instanceof HttpException) { status = exception.getStatus(); const exceptionResponse = exception.getResponse();
if (typeof exceptionResponse === 'object') { message = (exceptionResponse as any).message || exception.message; error = (exceptionResponse as any).error || 'Error'; details = (exceptionResponse as any).details; } else { message = exceptionResponse; error = 'Error'; } } else if (exception instanceof QueryFailedError) { status = HttpStatus.INTERNAL_SERVER_ERROR; message = 'Database error'; error = 'Database Error';
// Handle specific database errors const dbError = exception as any; if (dbError.code === '23505') { status = HttpStatus.CONFLICT; message = 'Resource already exists'; error = 'Conflict'; } else if (dbError.code === '23503') { status = HttpStatus.BAD_REQUEST; message = 'Referenced resource not found'; error = 'Bad Request'; }
this.logger.error({ message: 'Database query failed', query: dbError.query, parameters: dbError.parameters, error: dbError.message, code: dbError.code, }); } else if (exception instanceof Error) { status = HttpStatus.INTERNAL_SERVER_ERROR; message = 'Internal server error'; error = 'Internal Server Error';
this.logger.error({ message: exception.message, stack: exception.stack, }); } else { status = HttpStatus.INTERNAL_SERVER_ERROR; message = 'Unknown error'; error = 'Internal Server Error'; }
const errorResponse: ErrorResponse = { statusCode: status, message, error, timestamp: new Date().toISOString(), path: request.url, requestId: (request as any).context?.requestId, };
if (details) { errorResponse.details = details; }
response.status(status).json(errorResponse); }}
// Register globallyasync function bootstrap() { const app = await NestFactory.create(AppModule); app.useGlobalFilters(new AllExceptionsFilter()); await app.listen(3000);}TypeORM Exception Filter
Section titled “TypeORM Exception Filter”import { ExceptionFilter, Catch, ArgumentsHost, HttpStatus, Logger,} from '@nestjs/common';import { Response } from 'express';import { QueryFailedError, EntityNotFoundError } from 'typeorm';
@Catch(QueryFailedError, EntityNotFoundError)export class TypeOrmExceptionFilter implements ExceptionFilter { private logger = new Logger('TypeORM');
catch(exception: QueryFailedError | EntityNotFoundError, host: ArgumentsHost) { const ctx = host.switchToHttp(); const response = ctx.getResponse<Response>(); const request = ctx.getRequest();
let status = HttpStatus.INTERNAL_SERVER_ERROR; let message = 'Database error';
if (exception instanceof QueryFailedError) { const error = exception as any;
switch (error.code) { case '23505': // Unique violation status = HttpStatus.CONFLICT; message = this.parseUniqueViolation(error); break; case '23503': // Foreign key violation status = HttpStatus.BAD_REQUEST; message = 'Referenced resource does not exist'; break; case '23502': // Not null violation status = HttpStatus.BAD_REQUEST; message = 'Required field is missing'; break; case '23514': // Check violation status = HttpStatus.BAD_REQUEST; message = 'Data validation failed'; break; default: this.logger.error({ message: 'Unhandled database error', code: error.code, detail: error.detail, }); } } else if (exception instanceof EntityNotFoundError) { status = HttpStatus.NOT_FOUND; message = 'Resource not found'; }
response.status(status).json({ statusCode: status, message, timestamp: new Date().toISOString(), path: request.url, }); }
private parseUniqueViolation(error: any): string { const detail = error.detail;
// Parse: "Key (email)=(test@example.com) already exists." const match = detail.match(/Key \((.+?)\)=\((.+?)\) already exists/);
if (match) { return `${match[1]} '${match[2]}' already exists`; }
return 'Resource already exists'; }}38.6 Custom Exceptions
Section titled “38.6 Custom Exceptions”Domain Exceptions
Section titled “Domain Exceptions”import { HttpException, HttpStatus } from '@nestjs/common';
export class DomainException extends HttpException { constructor( message: string, public readonly code: string, public readonly details?: any, status: HttpStatus = HttpStatus.BAD_REQUEST, ) { super( { message, error: 'Domain Error', code, details, }, status, ); }}
// src/common/exceptions/entity-not-found.exception.tsimport { NotFoundException } from '@nestjs/common';
export class EntityNotFoundException extends NotFoundException { constructor(entityName: string, id: string | number) { super({ message: `${entityName} with ID ${id} not found`, error: 'Not Found', code: 'ENTITY_NOT_FOUND', }); }}
// src/common/exceptions/duplicate-entity.exception.tsimport { ConflictException } from '@nestjs/common';
export class DuplicateEntityException extends ConflictException { constructor(entityName: string, field: string, value: string) { super({ message: `${entityName} with ${field} '${value}' already exists`, error: 'Conflict', code: 'DUPLICATE_ENTITY', }); }}
// Usage in service@Injectable()export class UsersService { async findOne(id: number): Promise<User> { const user = await this.userRepository.findOne({ where: { id } });
if (!user) { throw new EntityNotFoundException('User', id); }
return user; }
async create(dto: CreateUserDto): Promise<User> { const existing = await this.userRepository.findOne({ where: { email: dto.email }, });
if (existing) { throw new DuplicateEntityException('User', 'email', dto.email); }
return this.userRepository.save(dto); }}38.7 Error Handling Best Practices
Section titled “38.7 Error Handling Best Practices”Service Layer Error Handling
Section titled “Service Layer Error Handling”import { Injectable, Logger, NotFoundException, ConflictException, InternalServerErrorException,} from '@nestjs/common';import { InjectRepository } from '@nestjs/typeorm';import { Repository, QueryFailedError } from 'typeorm';import { User } from './user.entity';
@Injectable()export class UsersService { private logger = new Logger(UsersService.name);
constructor( @InjectRepository(User) private userRepository: Repository<User>, ) {}
async create(dto: CreateUserDto): Promise<User> { try { const user = this.userRepository.create(dto); return await this.userRepository.save(user); } catch (error) { if (error instanceof QueryFailedError) { const dbError = error as any;
if (dbError.code === '23505') { throw new ConflictException('Email already exists'); }
this.logger.error('Database error during user creation', { error: dbError.message, code: dbError.code, }); }
throw new InternalServerErrorException('Failed to create user'); } }
async findOne(id: number): Promise<User> { const user = await this.userRepository.findOne({ where: { id } });
if (!user) { throw new NotFoundException(`User with ID ${id} not found`); }
return user; }
async update(id: number, dto: UpdateUserDto): Promise<User> { try { const user = await this.findOne(id); Object.assign(user, dto); return await this.userRepository.save(user); } catch (error) { if (error instanceof NotFoundException) { throw error; }
this.logger.error(`Failed to update user ${id}`, error); throw new InternalServerErrorException('Failed to update user'); } }}38.8 Summary
Section titled “38.8 Summary” Logging & Error Handling Quick Reference +------------------------------------------------------------------+ | | | Log Level | Use Case | | -------------------|------------------------------------------| | error | Application errors, exceptions | | warn | Warning conditions, deprecated features | | info | Important business events | | debug | Debug information, query details | | verbose | Detailed tracing | | | | Exception Type | HTTP Status | | -------------------|------------------------------------------| | BadRequestException| 400 | | UnauthorizedException| 401 | | ForbiddenException| 403 | | NotFoundException| 404 | | ConflictException| 409 | | InternalServerErrorException| 500 | | | | Best Practices | Description | | -------------------|------------------------------------------| | Structured logging| Use JSON format for logs | | Request context | Include requestId, userId | | Global filters | Catch all exceptions | | Custom exceptions| Domain-specific errors | | Log sensitive data| Never log passwords, tokens | | | +------------------------------------------------------------------+Next Chapter
Section titled “Next Chapter”Chapter 39: Deployment Strategies
Last Updated: February 2026