Error Handling in Adonis.js with Service Classes and Controllers
Here’s a comprehensive approach to handling different types of errors in Adonis.js applications using service classes and controllers:
1. Custom Error Classes
First, create custom error classes to represent different error types:
// app/Exceptions/AppError.ts
import { HttpException } from '@adonisjs/core/build/exceptions'
export class AppError extends HttpException {
constructor(
public message: string,
public status: number = 400,
public code: string = 'E_GENERIC_ERROR'
) {
super(message, status, code)
}
}
// app/Exceptions/NotFoundError.ts
export class NotFoundError extends AppError {
constructor(message: string = 'Resource not found') {
super(message, 404, 'E_NOT_FOUND')
}
}
// app/Exceptions/ValidationError.ts
export class ValidationError extends AppError {
constructor(errors: any) {
super('Validation failed', 422, 'E_VALIDATION_ERROR')
this.data = errors
}
}
// app/Exceptions/InternalServerError.ts
export class InternalServerError extends AppError {
constructor(message: string = 'Internal server error') {
super(message, 500, 'E_INTERNAL_ERROR')
}
}
2. Service Class with Error Handling
Create a service class that throws appropriate errors:
// app/Services/UserService.ts
import User from 'App/Models/User'
import { NotFoundError, InternalServerError, ValidationError } from 'App/Exceptions'
import { DateTime } from 'luxon'
export default class UserService {
private user: User
constructor(user?: User) {
this.user = user
}
/**
* Create a new user
*/
async createUser(data: any) {
try {
// Validate required fields
const requiredFields = ['email', 'name']
const missingFields = requiredFields.filter(field => !data[field])
if (missingFields.length > 0) {
throw new ValidationError({
message: `Missing required fields: ${missingFields.join(', ')}`
})
}
// Check if email already exists
const existingUser = await User.findBy('email', data.email)
if (existingUser) {
throw new ValidationError({
email: ['Email address already exists']
})
}
// Create user
const user = await User.create({
...data,
createdAt: DateTime.local(),
})
return user
} catch (error) {
// Re-throw known error types
if (error instanceof ValidationError ||
error instanceof InternalServerError ||
error instanceof NotFoundError) {
throw error
}
// Wrap unknown errors
throw new InternalServerError('Failed to create user')
}
}
/**
* Find user by ID
*/
async findById(id: number) {
try {
const user = await User.find(id)
if (!user) {
throw new NotFoundError(`User with ID ${id} not found`)
}
return user
} catch (error) {
if (error instanceof NotFoundError) {
throw error
}
throw new InternalServerError('Failed to fetch user')
}
}
/**
* Update user
*/
async updateUser(id: number, data: Partial<User>) {
try {
const user = await this.findById(id) // This will throw NotFoundError if needed
// Validate update data
if (data.email && data.email !== user.email) {
const existingUser = await User.findBy('email', data.email)
if (existingUser) {
throw new ValidationError({
email: ['Email address already exists']
})
}
}
// Update user
user.merge(data)
await user.save()
return user
} catch (error) {
if (error instanceof NotFoundError ||
error instanceof ValidationError) {
throw error
}
throw new InternalServerError('Failed to update user')
}
}
/**
* Delete user
*/
async deleteUser(id: number) {
try {
const user = await this.findById(id)
await user.delete()
return { success: true, message: 'User deleted successfully' }
} catch (error) {
if (error instanceof NotFoundError) {
throw error
}
throw new InternalServerError('Failed to delete user')
}
}
}
3. Controller with Error Handling
Create a controller that handles service errors and formats responses:
// app/Controllers/Http/UsersController.ts
import type { HttpContext } from '@adonisjs/core/http'
import UserService from 'App/Services/UserService'
import { NotFoundError, ValidationError } from 'App/Exceptions'
import { Logger } from '@adonisjs/core/services'
export default class UsersController {
private logger: Logger
constructor() {
this.logger = Logger.withName('users-controller')
}
/**
* Create a new user
*/
async store({ request, response }: HttpContext) {
try {
const userData = request.only(['name', 'email', 'password'])
const userService = new UserService()
const user = await userService.createUser(userData)
return response.created({
success: true,
data: {
user: user.serialize(),
},
message: 'User created successfully'
})
} catch (error) {
return this.handleError(error, response)
}
}
/**
* Show user by ID
*/
async show({ params, response }: HttpContext) {
try {
const userService = new UserService()
const user = await userService.findById(params.id)
return response.ok({
success: true,
data: {
user: user.serialize(),
}
})
} catch (error) {
return this.handleError(error, response)
}
}
/**
* Update user
*/
async update({ params, request, response }: HttpContext) {
try {
const updateData = request.only(['name', 'email'])
const userService = new UserService()
const user = await userService.updateUser(params.id, updateData)
return response.ok({
success: true,
data: {
user: user.serialize(),
},
message: 'User updated successfully'
})
} catch (error) {
return this.handleError(error, response)
}
}
/**
* Delete user
*/
async destroy({ params, response }: HttpContext) {
try {
const userService = new UserService()
await userService.deleteUser(params.id)
return response.noContent()
} catch (error) {
return this.handleError(error, response)
}
}
/**
* Centralized error handling
*/
private handleError(error: any, response: HttpContext['response']) {
this.logger.error(error)
// Handle custom application errors
if (error instanceof NotFoundError) {
return response.status(error.status).json({
success: false,
message: error.message,
code: error.code,
timestamp: new Date().toISOString()
})
}
if (error instanceof ValidationError) {
return response.status(error.status).json({
success: false,
message: error.message,
code: error.code,
errors: error.data,
timestamp: new Date().toISOString()
})
}
if (error instanceof InternalServerError) {
// Don't expose internal error details in production
const publicMessage = process.env.NODE_ENV === 'production'
? 'An unexpected error occurred'
: error.message
return response.status(error.status).json({
success: false,
message: publicMessage,
code: error.code,
timestamp: new Date().toISOString()
})
}
// Handle validation errors from AdonisJS
if (error.name === 'ValidationException') {
return response.status(422).json({
success: false,
message: 'Validation failed',
code: 'E_VALIDATION_ERROR',
errors: error.messages,
timestamp: new Date().toISOString()
})
}
// Handle generic errors
return response.status(500).json({
success: false,
message: 'Internal server error',
code: 'E_INTERNAL_ERROR',
timestamp: new Date().toISOString()
})
}
}
4. Global Error Handler (Optional)
For additional error handling, you can also create a global exception handler:
// start/exceptions.ts
import { Exception } from '@adonisjs/core/build/exceptions'
import { Logger } from '@adonisjs/core/services'
import { NotFoundError, InternalServerError } from 'App/Exceptions'
/*
|--------------------------------------------------------------------------
| Exception Handler
|--------------------------------------------------------------------------
*/
const logger = Logger.withName('app')
export default class HttpExceptionHandler extends Exception {
constructor() {
super()
}
async handle(error: any, { request, response }: any) {
logger.error(error)
// Handle custom application errors
if (error instanceof NotFoundError) {
return response.status(error.status).json({
success: false,
message: error.message,
code: error.code,
timestamp: new Date().toISOString()
})
}
if (error instanceof InternalServerError) {
const publicMessage = process.env.NODE_ENV === 'production'
? 'An unexpected error occurred'
: error.message
return response.status(error.status).json({
success: false,
message: publicMessage,
code: error.code,
timestamp: new Date().toISOString()
})
}
// Handle other exceptions
return super.handle(error, { request, response })
}
}
5. Example Response Formats
Success Response:
{
"success": true,
"data": {
"user": {
"id": 1,
"name": "John Doe",
"email": "john@example.com"
}
},
"message": "User created successfully"
}
Not Found Error:
{
"success": false,
"message": "User with ID 123 not found",
"code": "E_NOT_FOUND",
"timestamp": "2025-01-15T10:30:00.000Z"
}
Validation Error:
{
"success": false,
"message": "Validation failed",
"code": "E_VALIDATION_ERROR",
"errors": {
"email": ["Email address already exists"]
},
"timestamp": "2025-01-15T10:30:00.000Z"
}
Internal Server Error:
{
"success": false,
"message": "An unexpected error occurred",
"code": "E_INTERNAL_ERROR",
"timestamp": "2025-01-15T10:30:00.000Z"
}
Key Benefits of This Approach:
- Separation of Concerns: Services handle business logic and throw meaningful errors
- Consistent Error Handling: Controllers format errors consistently
- Type Safety: Custom error classes provide type safety
- Environment Awareness: Different error messages for development vs production
- Logging: All errors are logged for debugging
- Standardized Responses: Consistent API response format
- Extensibility: Easy to add new error types
This pattern ensures your Adonis.js application has robust, maintainable error handling that provides clear feedback to clients while keeping your services focused on business logic.