In Node.js, there are various ways to handle errors. In this blog, I will show you how to handle them in a very easy way and get rid of all try-catch blocks.
Setup
Let’s assume we have a simple Node.js (Express) server running. Now install npm packages called express-async-errors
and http-status-codes
.
npm i express-async-errors
express-async-errors
is a small package which does most of the heavy lifting. Let’s use this package in the root file, and it should always be at the top.
require('express-async-errors'); const express = require('express'); const app = express(); app.use(express.json()); const port = process.env.PORT || 5000; app.listen(port, () => { console.log(`server is listening on port ${port}...`); });
Custom Error Utility
Let’s create utility functions to throw different errors.
const { StatusCodes } = require("http-status-codes"); class CustomAPIError extends Error { constructor(message) { super(message); } } class BadRequestError extends CustomAPIError { constructor(message) { super(message); this.statusCode = StatusCodes.BAD_REQUEST; } } class ConflictError extends CustomAPIError { constructor(message) { super(message); this.statusCode = StatusCodes.CONFLICT; } } class NotFoundError extends CustomAPIError { constructor(message) { super(message); this.statusCode = StatusCodes.NOT_FOUND; } } class UnauthenticatedError extends CustomAPIError { constructor(message) { super(message); this.statusCode = StatusCodes.UNAUTHORIZED; } } class UnauthorizedError extends CustomAPIError { constructor(message) { super(message); this.statusCode = StatusCodes.FORBIDDEN; } } module.exports = {CustomAPIError, BadRequestError, ConflictError, NotFoundError, UnauthenticatedError, UnauthorizedError}
Middleware
const { StatusCodes } = require('http-status-codes'); const { CustomAPIError } = require('../errors'); const errorHandlerMiddleware = (err, req, res, next) => { const customError = { msg: 'Something went wrong, please try again', statusCode: StatusCodes.INTERNAL_SERVER_ERROR, }; if (err instanceof CustomAPIError) { customError.msg = err.message; customError.statusCode = err.statusCode; } res.status(customError.statusCode).json({ msg: customError.msg }); }; module.exports = errorHandlerMiddleware;
Optional Checks
You can add some optional checks too. I will show some examples for Prisma and Mongoose.
Prisma
const { StatusCodes } = require('http-status-codes'); const { Prisma } = require('@prisma/client'); const { CustomAPIError } = require('../errors'); const errorHandlerMiddleware = (err, req, res, next) => { const customError = { msg: 'Something went wrong, please try again', statusCode: StatusCodes.INTERNAL_SERVER_ERROR, }; if (err instanceof CustomAPIError) { customError.msg = err.message; customError.statusCode = err.statusCode; } if (err instanceof Prisma.PrismaClientKnownRequestError) { switch (err.code) { case 'P2000': { const key = err.meta.column_name; customError.msg = `${key} is too long`; customError.statusCode = StatusCodes.BAD_REQUEST; break; } case 'P2001': { customError.msg = 'Not found'; customError.statusCode = StatusCodes.NOT_FOUND; break; } case 'P2002': { const key = err.meta.target[0]; customError.msg = `Provided ${key} already exists`; customError.statusCode = StatusCodes.CONFLICT; break; } case 'P2003': { const key = err.meta.field_name; customError.msg = `${key} does not exist`; customError.statusCode = StatusCodes.NOT_FOUND; break; } case 'P2025': { customError.msg = 'No record found to delete'; customError.statusCode = StatusCodes.NOT_FOUND; break; } default: { customError.msg = 'Something went wrong, please try again'; customError.statusCode = StatusCodes.INTERNAL_SERVER_ERROR; } } } res.status(customError.statusCode).json({ msg: customError.msg }); }; module.exports = errorHandlerMiddleware;
Mongoose
const { StatusCodes } = require('http-status-codes'); const mongoose = require('mongoose'); const { CustomAPIError } = require('../errors'); const errorHandlerMiddleware = (err, req, res, next) => { const customError = { msg: 'Something went wrong, please try again', statusCode: StatusCodes.INTERNAL_SERVER_ERROR, }; if (err instanceof CustomAPIError) { customError.msg = err.message; customError.statusCode = err.statusCode; } if (err instanceof mongoose.Error.ValidationError) { if (err.name === "ValidationError") { customError.msg = Object.values(err.errors) .map((item) => item.message) .join(","); customError.statusCode = StatusCodes.BAD_REQUEST; } if (err.code && err.code === 11000) { customError.msg = `Duplicate value entered for ${Object.keys( err.keyValue )} field, please choose another value`; customError.statusCode = StatusCodes.BAD_REQUEST; } if (err.name === "CastError") { customError.msg = `No match found with id of ${err.value}`; customError.statusCode = StatusCodes.NOT_FOUND; } } res.status(customError.statusCode).json({ msg: customError.msg }); }; module.exports = errorHandlerMiddleware;
That’s It
Now use that middleware below all API routes. We don’t have to wrap any async code block inside try-catch. All the errors will be automatically caught by our middleware.
require('express-async-errors'); const express = require('express'); const errorHandlerMiddleware = require('./middleware/error-handler'); const app = express(); app.use(express.json()); /* api routes */ ... app.use(errorHandlerMiddleware); const port = process.env.PORT || 5000; app.listen(port, () => { console.log(`server is listening on port ${port}...`); });
Hope that helps! Let me know if you have any further questions or need more clarification!