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:
| Class | Default 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.";
