Error Handling & Logging
This guide covers best practices for error handling and logging in our NestJS applications.
Error Handling
1. Use GraphQL Exceptions
// Use built-in GraphQL exceptions
import {
BadRequestException,
NotFoundException,
UnauthorizedException,
ForbiddenException,
ConflictException,
InternalServerErrorException
} from '@nestjs/common';
// Usage examples with context
throw new NotFoundException({
message: 'User not found',
error: 'NOT_FOUND',
resourceId: userId,
resourceType: 'User'
});
throw new BadRequestException({
message: 'Invalid input data',
error: 'VALIDATION_ERROR',
field: 'email',
value: email
});
throw new UnauthorizedException({
message: 'Authentication required',
error: 'UNAUTHORIZED',
action: 'access_user_profile'
});
throw new ForbiddenException({
message: 'Insufficient permissions',
error: 'FORBIDDEN',
resourceId: userId,
requiredPermission: 'ADMIN'
});
throw new ConflictException({
message: 'Resource already exists',
error: 'CONFLICT',
resourceId: email,
resourceType: 'User'
});
throw new InternalServerErrorException({
message: 'Database connection failed',
error: 'INTERNAL_ERROR',
service: 'Database',
operation: 'create_user'
});
2. REST API Error Handling (Non-GraphQL Apps)
For applications that don't use GraphQL, use standard HTTP exceptions with structured responses:
// REST API error handling
import {
HttpException,
HttpStatus
} from '@nestjs/common';
// Custom error response structure
interface ErrorResponse {
statusCode: number;
message: string;
error: string;
timestamp: string;
path: string;
details?: any;
}
// Usage examples for REST APIs
throw new HttpException({
statusCode: HttpStatus.NOT_FOUND,
message: 'User not found',
error: 'Not Found',
timestamp: new Date().toISOString(),
path: '/api/users/123',
details: {
userId: '123',
resourceType: 'User'
}
}, HttpStatus.NOT_FOUND);
throw new HttpException({
statusCode: HttpStatus.BAD_REQUEST,
message: 'Validation failed',
error: 'Bad Request',
timestamp: new Date().toISOString(),
path: '/api/users',
details: {
field: 'email',
value: 'invalid-email',
constraint: 'email_format'
}
}, HttpStatus.BAD_REQUEST);
throw new HttpException({
statusCode: HttpStatus.CONFLICT,
message: 'User already exists',
error: 'Conflict',
timestamp: new Date().toISOString(),
path: '/api/users',
details: {
resourceId: 'user@example.com',
resourceType: 'User',
constraint: 'email_unique'
}
}, HttpStatus.CONFLICT);
3. Global Exception Filter
// filters/http-exception.filter.ts
@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 errorResponse = {
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
message: exception.message,
error: exception.getResponse(),
};
// Log the error
this.logger.error('HTTP Exception', {
...errorResponse,
stack: exception.stack,
});
response.status(status).json(errorResponse);
}
}
4. Validation Errors
// pipes/validation.pipe.ts
@Injectable()
export class ValidationPipe extends DefaultValidationPipe {
transform(value: any, metadata: ArgumentMetadata) {
try {
return super.transform(value, metadata);
} catch (error) {
if (error instanceof BadRequestException) {
throw new BadRequestException({
message: 'Validation failed',
error: 'VALIDATION_ERROR',
details: error.getResponse()
});
}
throw error;
}
}
}
5. Async Error Handling
// Always use try-catch with async operations
async createUser(userData: CreateUserDto): Promise<User> {
try {
const user = await this.userRepository.create(userData);
return user;
} catch (error) {
this.logger.error('Failed to create user', {
error: error.message,
userData,
stack: error.stack,
});
if (error.code === '23505') { // PostgreSQL unique constraint
throw new ConflictException({
message: 'User already exists',
error: 'CONFLICT',
resourceId: userData.email,
resourceType: 'User',
constraint: 'email_unique'
});
}
throw new InternalServerErrorException({
message: 'Internal server error',
error: 'INTERNAL_ERROR',
operation: 'create_user',
details: error.message
});
}
}
Logging
1. Winston Configuration
// config/winston.config.ts
import { WinstonModule } from 'nest-winston';
import * as winston from 'winston';
export const winstonConfig = WinstonModule.createLogger({
transports: [
// Console transport for development
new winston.transports.Console({
format: winston.format.combine(
winston.format.timestamp(),
winston.format.colorize(),
winston.format.simple()
),
}),
// Google Cloud Logging
new winston.transports.Console({
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
}),
],
level: process.env.LOG_LEVEL || 'info',
});
2. Structured Logging
// Always use structured logging with context
@Injectable()
export class UserService {
constructor(private readonly logger: Logger) {}
async findUser(id: string): Promise<User> {
this.logger.log('Finding user', {
userId: id,
service: 'UserService',
method: 'findUser',
});
try {
const user = await this.userRepository.findById(id);
this.logger.log('User found', {
userId: id,
userEmail: user.email,
service: 'UserService',
method: 'findUser',
});
return user;
} catch (error) {
this.logger.error('Failed to find user', {
userId: id,
error: error.message,
service: 'UserService',
method: 'findUser',
stack: error.stack,
});
throw error;
}
}
}
3. Log Levels
// Use appropriate log levels
logger.error('Critical error that needs immediate attention', { error, context });
logger.warn('Warning that should be monitored', { warning, context });
logger.info('General information about application flow', { info, context });
logger.debug('Detailed debugging information', { debug, context });
logger.verbose('Very detailed debugging information', { verbose, context });
4. Request Logging Interceptor
// interceptors/logging.interceptor.ts
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
constructor(private readonly logger: Logger) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const { method, url, body, user } = request;
const now = Date.now();
this.logger.log('Incoming request', {
method,
url,
body: this.sanitizeBody(body),
userId: user?.id,
userAgent: request.get('User-Agent'),
});
return next.handle().pipe(
tap(() => {
const responseTime = Date.now() - now;
this.logger.log('Request completed', {
method,
url,
responseTime: `${responseTime}ms`,
});
}),
catchError((error) => {
const responseTime = Date.now() - now;
this.logger.error('Request failed', {
method,
url,
responseTime: `${responseTime}ms`,
error: error.message,
});
throw error;
})
);
}
private sanitizeBody(body: any): any {
// Remove sensitive data from logs
const sanitized = { ...body };
delete sanitized.password;
delete sanitized.token;
return sanitized;
}
}
5. Future: Elasticsearch Integration
// config/elasticsearch-logger.config.ts
import { ElasticsearchTransport } from 'winston-elasticsearch';
const elasticsearchTransport = new ElasticsearchTransport({
level: 'info',
clientOpts: {
node: process.env.ELASTICSEARCH_URL,
auth: {
username: process.env.ELASTICSEARCH_USERNAME,
password: process.env.ELASTICSEARCH_PASSWORD,
},
},
indexPrefix: 'logs',
ensureMappingTemplate: true,
mappingTemplate: {
index_patterns: ['logs-*'],
settings: {
number_of_shards: 1,
number_of_replicas: 0,
},
},
});
// Add to winston config
transports: [
// ... existing transports
elasticsearchTransport,
]
Best Practices
Error Handling
- Use built-in GraphQL exceptions for consistent error responses
- Implement global exception filters for centralized error handling
- Handle async errors with try-catch blocks
- Don't expose internal errors to clients
- Log all errors with sufficient context
Logging
- Use structured logging with consistent fields
- Include relevant context (userId, requestId, service, method)
- Sanitize sensitive data before logging
- Use appropriate log levels
- Log request/response cycles for debugging
- Include stack traces for errors
- Use correlation IDs for request tracing
Performance
- Avoid logging in hot paths
- Use async logging when possible
- Batch log entries for better performance
- Monitor log volume and storage costs