Node / Express Guide
Error Handling
The HttpError hierarchy, throwing errors, and the global error handler.
HttpError Class Hierarchy
All operational errors extend HttpError from @codisolutions23/node-utils:
HttpError (base — isOperational: true)
├── BadRequestError 400
├── UnauthorizedError 401
├── ForbiddenError 403
├── NotFoundError 404
├── ConflictError 409
├── UnprocessableEntityError 422
└── InternalServerError 500
Throwing Errors in Services
Throw HttpError subclasses from services — never from controllers:
import {
NotFoundError,
ConflictError,
UnauthorizedError,
UnprocessableEntityError,
} from '@codisolutions23/node-utils'
async function createUser(input: TUserCreate) {
const existing = await userRepo.getUserByEmail(input.email)
if (existing) throw new ConflictError('Email already in use.')
return userRepo.createUser(input)
}
async function login(input: TLoginInput) {
const user = await userRepo.getUserByEmail(input.email)
if (!user) throw new NotFoundError('User not found.')
const match = await comparePasswords(input.password, user.password!)
if (!match) throw new UnauthorizedError('Invalid credentials.')
}
Controller Error Flow
Controllers never construct error responses directly. They validate with Joi, then delegate, then catch → next(error):
// ✅ Controller
async function create(req, res, next) {
try {
const { error, value } = createSchema.validate(req.body, { abortEarly: false })
if (error) return next(new UnprocessableEntityError(error.message)) // Joi error
const result = await service.create(value)
res.status(201).json(result)
} catch (error) {
log('error', resource, 'create', 'Failed.', { error })
return next(error) // passes HttpError (or unexpected Error) to errorHandler
}
}
Global Error Handler
Register errorHandler from @codisolutions23/node-utils last in src/app.ts:
import { errorHandler } from '@codisolutions23/node-utils'
// All routes above
app.use(errorHandler)
The handler's behaviour:
export const errorHandler = (error: HttpError, req, res, next) => {
if (error.isOperational) {
// Known HttpError — send structured response
res.status(error.statusCode).json({
status: 'error',
message: error.message,
})
} else {
// Unexpected error — log and return generic 500
logger.error('Unexpected error', { error })
res.status(500).json({
status: 'error',
message: 'An unexpected error occurred.',
})
}
}
Error Response Shape
All error responses follow this shape:
{
"status": "error",
"message": "Human-readable description of what went wrong."
}
Examples:
// 422 Unprocessable Entity
{ "status": "error", "message": "\"email\" must be a valid email address." }
// 401 Unauthorized
{ "status": "error", "message": "Invalid credentials." }
// 404 Not Found
{ "status": "error", "message": "User not found." }
// 409 Conflict
{ "status": "error", "message": "Email already in use." }
Frontend Handling
Read the message field from the error response on the client:
// In a composable or handleApiAction catch block
const msg = error?.data?.message || error?.message || 'An error occurred.'