Error Handling
HonestJS provides a comprehensive error handling system that allows you to catch, process, and respond to errors in a consistent and organized way.
Overview
The error handling system in HonestJS consists of several components:
- Exception Filters: Classes that catch and handle specific types of exceptions
- Global Error Handlers: Application-wide error handling configuration
- HTTP Exceptions: Built-in exception types for HTTP-specific errors
- Error Response Formatting: Standardized error response structure
Exception Filters
Exception filters are the primary way to handle errors in HonestJS. They catch exceptions thrown during request processing and can return custom responses.
Creating Exception Filters
import type { IFilter } from 'honestjs'
import type { Context } from 'hono'
export class HttpExceptionFilter implements IFilter {
async catch(exception: Error, context: Context) {
console.error('HTTP Exception:', exception)
return context.json(
{
status: 500,
message: 'Internal Server Error',
timestamp: new Date().toISOString(),
path: context.req.path,
},
500
)
}
}
export class ValidationExceptionFilter implements IFilter {
async catch(exception: Error, context: Context) {
console.error('Validation Error:', exception)
return context.json(
{
status: 400,
message: 'Validation Error',
details: exception.message,
timestamp: new Date().toISOString(),
path: context.req.path,
},
400
)
}
}
Applying Exception Filters
import { Controller, Get, UseFilters } from 'honestjs'
import { HttpExceptionFilter, ValidationExceptionFilter } from './filters'
@Controller('users')
@UseFilters(HttpExceptionFilter, ValidationExceptionFilter)
class UsersController {
@Get(':id')
async getUser(@Param('id') id: string) {
if (!id) {
throw new Error('User ID is required')
}
const user = await this.usersService.findById(id)
if (!user) {
throw new Error('User not found')
}
return user
}
}
Filter-Specific Exception Handling
You can create filters that handle specific types of exceptions:
export class DatabaseExceptionFilter implements IFilter {
async catch(exception: Error, context: Context) {
if (exception.message.includes('database') || exception.message.includes('connection')) {
console.error('Database Error:', exception)
return context.json(
{
status: 503,
message: 'Service Unavailable',
details: 'Database connection error',
timestamp: new Date().toISOString(),
path: context.req.path,
},
503
)
}
// Return undefined to let other filters handle it
return undefined
}
}
export class AuthenticationExceptionFilter implements IFilter {
async catch(exception: Error, context: Context) {
if (exception.message.includes('unauthorized') || exception.message.includes('authentication')) {
console.error('Authentication Error:', exception)
return context.json(
{
status: 401,
message: 'Unauthorized',
details: 'Authentication required',
timestamp: new Date().toISOString(),
path: context.req.path,
},
401
)
}
return undefined
}
}
HTTP Exceptions
HonestJS integrates with Hono's HTTPException for HTTP-specific errors:
import { HTTPException } from 'hono/http-exception'
import { Controller, Get, Param } from 'honestjs'
@Controller('users')
class UsersController {
@Get(':id')
async getUser(@Param('id') id: string) {
if (!id) {
throw new HTTPException(400, { message: 'User ID is required' })
}
const user = await this.usersService.findById(id)
if (!user) {
throw new HTTPException(404, { message: 'User not found' })
}
return user
}
@Post()
async createUser(@Body() userData: CreateUserDto) {
if (!userData.email) {
throw new HTTPException(422, {
message: 'Validation failed',
details: { email: 'Email is required' },
})
}
return await this.usersService.create(userData)
}
}
Global Error Handling
You can configure global error handling when creating your application:
import { Application } from 'honestjs'
import type { Context } from 'hono'
const { app, hono } = await Application.create(AppModule, {
// Custom error handler for unhandled exceptions
onError: (error: Error, context: Context) => {
console.error('Unhandled error:', error)
// Log error details
console.error('Stack trace:', error.stack)
console.error('Request path:', context.req.path)
console.error('Request method:', context.req.method)
// Return appropriate response based on environment
if (process.env.NODE_ENV === 'production') {
return context.json(
{
status: 500,
message: 'Internal Server Error',
timestamp: new Date().toISOString(),
path: context.req.path,
},
500
)
} else {
return context.json(
{
status: 500,
message: error.message,
stack: error.stack,
timestamp: new Date().toISOString(),
path: context.req.path,
},
500
)
}
},
// Custom not found handler
notFound: (context: Context) => {
return context.json(
{
status: 404,
message: 'Not Found',
details: `Route ${context.req.path} not found`,
timestamp: new Date().toISOString(),
suggestions: ['/api/users', '/api/posts', '/api/health'],
},
404
)
},
})
Error Response Format
HonestJS provides a standardized error response format:
interface ErrorResponse {
status: number
message: string
timestamp: string
path: string
requestId?: string
code?: string
details?: Record<string, any>
errors?: Array<{ property: string; constraints: Record<string, string> }>
}
Example Error Responses
{
"status": 400,
"message": "Validation Error",
"timestamp": "2024-01-01T12:00:00.000Z",
"path": "/api/users",
"code": "VALIDATION_ERROR",
"details": {
"email": "Invalid email format"
}
}
{
"status": 404,
"message": "User not found",
"timestamp": "2024-01-01T12:00:00.000Z",
"path": "/api/users/123",
"code": "NOT_FOUND"
}
{
"status": 500,
"message": "Internal Server Error",
"timestamp": "2024-01-01T12:00:00.000Z",
"path": "/api/users",
"requestId": "req-123",
"details": {
"stack": "Error: Database connection failed..."
}
}
Custom Error Classes
You can create custom error classes for better error handling:
export class ValidationError extends Error {
constructor(message: string, public field: string, public value: any) {
super(message)
this.name = 'ValidationError'
}
}
export class BusinessLogicError extends Error {
constructor(message: string, public code: string, public details?: Record<string, any>) {
super(message)
this.name = 'BusinessLogicError'
}
}
export class DatabaseError extends Error {
constructor(message: string, public operation: string, public table?: string) {
super(message)
this.name = 'DatabaseError'
}
}
Using Custom Error Classes
@Controller('users')
class UsersController {
@Post()
async createUser(@Body() userData: CreateUserDto) {
if (!userData.email) {
throw new ValidationError('Email is required', 'email', userData.email)
}
if (!this.isValidEmail(userData.email)) {
throw new ValidationError('Invalid email format', 'email', userData.email)
}
try {
return await this.usersService.create(userData)
} catch (error) {
if (error.message.includes('duplicate')) {
throw new BusinessLogicError('User already exists', 'USER_EXISTS', { email: userData.email })
}
throw error
}
}
}
Handling Custom Errors
export class ValidationExceptionFilter implements IFilter {
async catch(exception: Error, context: Context) {
if (exception instanceof ValidationError) {
return context.json(
{
status: 400,
message: 'Validation Error',
code: 'VALIDATION_ERROR',
details: {
field: exception.field,
value: exception.value,
message: exception.message,
},
timestamp: new Date().toISOString(),
path: context.req.path,
},
400
)
}
return undefined
}
}
export class BusinessLogicExceptionFilter implements IFilter {
async catch(exception: Error, context: Context) {
if (exception instanceof BusinessLogicError) {
return context.json(
{
status: 409,
message: exception.message,
code: exception.code,
details: exception.details,
timestamp: new Date().toISOString(),
path: context.req.path,
},
409
)
}
return undefined
}
}
Error Handling Best Practices
1. Use Appropriate HTTP Status Codes
// ✅ Good - Use appropriate status codes
throw new HTTPException(400, { message: 'Bad Request' })
throw new HTTPException(401, { message: 'Unauthorized' })
throw new HTTPException(403, { message: 'Forbidden' })
throw new HTTPException(404, { message: 'Not Found' })
throw new HTTPException(422, { message: 'Validation Error' })
throw new HTTPException(500, { message: 'Internal Server Error' })
// ❌ Avoid - Don't use generic 500 for client errors
throw new HTTPException(500, { message: 'Validation Error' })
2. Provide Meaningful Error Messages
// ✅ Good - Clear, actionable error messages
throw new Error('User ID is required')
throw new Error('Email must be a valid email address')
throw new Error('Password must be at least 8 characters long')
// ❌ Avoid - Vague error messages
throw new Error('Invalid input')
throw new Error('Error occurred')
3. Include Request Context
export class LoggerExceptionFilter implements IFilter {
async catch(exception: Error, context: Context) {
console.error('Error details:', {
message: exception.message,
stack: exception.stack,
path: context.req.path,
method: context.req.method,
headers: Object.fromEntries(context.req.header()),
timestamp: new Date().toISOString(),
})
return undefined // Let other filters handle the response
}
}
4. Handle Different Environments
export class ProductionExceptionFilter implements IFilter {
async catch(exception: Error, context: Context) {
if (process.env.NODE_ENV === 'production') {
// Don't expose internal details in production
return context.json(
{
status: 500,
message: 'Internal Server Error',
timestamp: new Date().toISOString(),
path: context.req.path,
},
500
)
}
// Show detailed error in development
return context.json(
{
status: 500,
message: exception.message,
stack: exception.stack,
timestamp: new Date().toISOString(),
path: context.req.path,
},
500
)
}
}
5. Use Request IDs for Tracking
export class RequestIdMiddleware implements IMiddleware {
async use(c: Context, next: Next) {
const requestId = c.req.header('x-request-id') || generateRequestId()
c.set('requestId', requestId)
// Add request ID to response headers
c.header('x-request-id', requestId)
await next()
}
}
export class ErrorTrackingFilter implements IFilter {
async catch(exception: Error, context: Context) {
const requestId = context.get('requestId')
console.error(`[${requestId}] Error:`, {
message: exception.message,
stack: exception.stack,
path: context.req.path,
})
return context.json(
{
status: 500,
message: 'Internal Server Error',
requestId,
timestamp: new Date().toISOString(),
path: context.req.path,
},
500
)
}
}
6. Chain Exception Filters
@Controller('users')
@UseFilters(
LoggerExceptionFilter, // Log all errors
ValidationExceptionFilter, // Handle validation errors
AuthenticationExceptionFilter, // Handle auth errors
HttpExceptionFilter // Handle HTTP exceptions
)
class UsersController {
// Your controller methods
}
By following these practices, you can create robust error handling that provides clear, actionable feedback to clients while maintaining security and debugging capabilities.