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.'