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

Default Messages

Each error class has a descriptive default message — you only need to pass a custom message when the default isn't specific enough:

ClassDefault message
BadRequestError"The request could not be processed. Please review your input and try again."
UnauthorizedError"Authentication is required to access this resource."
ForbiddenError"You do not have the necessary permissions to perform this action."
NotFoundError"The requested resource could not be found."
ConflictError"A resource with the provided values already exists."
UnprocessableEntityError"The request could not be completed due to invalid or incomplete information."
InternalServerError"An internal server error occurred. Please try again later."

Throwing Errors in Services

Throw HttpError subclasses from services — never from controllers:

import {
  NotFoundError,
  ConflictError,
  BadRequestError,
} 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({ email, password }: { email: string; password: string }) {
  const user = await userRepo.getUserByEmail({ email });
  if (!user) throw new NotFoundError("Invalid credentials.");

  const isValid = await comparePasswords(password, user.password || "");
  if (!isValid) throw new BadRequestError("Invalid credentials.");
}

Controller Error Flow

Controllers never construct error responses directly. They validate with Joi, then delegate, then catch → next(error):

async function create(
  req: AuthenticatedRequest,
  res: Response,
  next: NextFunction,
) {
  const method = "create";
  try {
    const { error, value } = createSchema.validate(req.body, { convert: true });
    if (error) return next(new UnprocessableEntityError(error.message));

    const result = await service.create(value);
    res.status(201).json(result);
  } catch (error: any) {
    log("error", resource, method, "Failed.", { error: error.message });
    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";

app.use("/api", router());
app.use(errorHandler); // must be last

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({ message: error.message });
    res.status(500).json({
      status: "error",
      message: new InternalServerError().message,
    });
  }
};
For unexpected (non-operational) errors, the handler uses new InternalServerError().message — which is "An internal server error occurred. Please try again later.". It does not expose the real error message to the client.

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": "Please provide a valid email address." }

// 400 Bad Request
{ "status": "error", "message": "Invalid credentials." }

// 404 Not Found
{ "status": "error", "message": "The requested resource could not be found." }

// 409 Conflict
{ "status": "error", "message": "A user with this email (john@example.com) already exists." }

Frontend Handling

Read the message field from the error response on the client:

const msg = error?.data?.message || error?.message || "An error occurred.";