DEV Community

T.M
T.M

Posted on Edited on

CUSTOM NODE JS ERROR HANDLING

Introduction

How we present errors and response messages to the client significantly impacts how easily they can handle and act on them. In a microservice architecture where different stacks are used for backend services, a consistent error response structure becomes essential—especially when all endpoints are consumed by a single frontend application.

Uniform error formatting allows frontend developers to implement a single, reliable error-handling strategy.

In this article, I'll show you how to build a standardized error response structure using TypeScript, Node.js, and Express.js.


Error Response Format

All error responses will follow this structure:

{
  "errors": [
    {
      "message": "Description",
      "field": "Referenced field, e.g., Email"
    },
    {
      "message": "Description",
      "field": "Referenced field, e.g., Name"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Initializing the Project

1. Create a new directory and initialize a Node project

mkdir error-handling-example
cd error-handling-example
npm init -y
Enter fullscreen mode Exit fullscreen mode

2. Install necessary dependencies

# TypeScript and linting
npm install -D typescript tslint

# Express and related packages
npm install express express-validator
npm install -D @types/express express-async-errors ts-node-dev
Enter fullscreen mode Exit fullscreen mode

3. Initialize TypeScript configuration

tsc --init
Enter fullscreen mode Exit fullscreen mode

This will generate a tsconfig.json file.


Project Structure

Create a src/ directory and open your project in your preferred IDE. Inside src/, organize files as follows:

src/
├── app.ts
├── routes/
│   └── routes.ts
├── controllers/
│   └── signup.ts
├── errors/
│   ├── custom-error.ts
│   └── request-validation-error.ts
├── middlewares/
│   └── error-handler.ts
Enter fullscreen mode Exit fullscreen mode

Creating the Server

In src/app.ts:

import express from "express";
import "express-async-errors";

import authentication from "./routes/routes";
import { NotFoundError } from "./errors/not-found-err";
import { errorHandler } from "./middlewares/error-handler";

const app = express();
app.use(express.json());

app.use("/api/users", authentication);

app.all("*", async () => {
  throw new NotFoundError();
});

app.use(errorHandler);

const port = 3000;
app.listen(port, () => {
  console.log(`Server is listening on port ${port}`);
});
Enter fullscreen mode Exit fullscreen mode

express-async-errors allows us to throw errors in async routes without wrapping them in try-catch.


Setting Up the Route

In src/routes/routes.ts:

import express from "express";
import { body } from "express-validator";
import { signUp } from "../controllers/signup";

const router = express.Router();

router.post(
  "/signup",
  [
    body("email").isEmail().withMessage("Invalid email"),
    body("password")
      .trim()
      .isLength({ min: 6, max: 20 })
      .withMessage("Password must be between 6 to 20 characters"),
  ],
  signUp
);

export default router;
Enter fullscreen mode Exit fullscreen mode

Creating the Controller

In src/controllers/signup.ts:

import { Request, Response } from "express";
import { validationResult } from "express-validator";
import { RequestValidationError } from "../errors/request-validation-error";

export const signUp = async (req: Request, res: Response) => {
  const errors = validationResult(req);

  if (!errors.isEmpty()) {
    throw new RequestValidationError(errors.array());
  }

  const { email, password } = req.body;

  // Add database operations or token logic here

  res.status(201).send({ email, password });
};
Enter fullscreen mode Exit fullscreen mode

Creating a Custom Error Class

In src/errors/custom-error.ts:

export abstract class CustomError extends Error {
  abstract statusCode: number;

  constructor(message: string) {
    super(message);
    Object.setPrototypeOf(this, CustomError.prototype);
  }

  abstract serializeErrors(): { message: string; field?: string }[];
}
Enter fullscreen mode Exit fullscreen mode

Handling Validation Errors

In src/errors/request-validation-error.ts:

import { ValidationError } from "express-validator";
import { CustomError } from "./custom-error";

export class RequestValidationError extends CustomError {
  statusCode = 400;

  constructor(public errors: ValidationError[]) {
    super("Invalid request parameters");
    Object.setPrototypeOf(this, RequestValidationError.prototype);
  }

  serializeErrors() {
    return this.errors.map(err => ({
      message: err.msg,
      field: err.param,
    }));
  }
}
Enter fullscreen mode Exit fullscreen mode

Creating the Error Handler Middleware

In src/middlewares/error-handler.ts:

import { Request, Response, NextFunction } from "express";
import { CustomError } from "../errors/custom-error";

export const errorHandler = (
  err: Error,
  req: Request,
  res: Response,
  next: NextFunction
) => {
  if (err instanceof CustomError) {
    return res.status(err.statusCode).send({ errors: err.serializeErrors() });
  }

  console.error(err);

  res.status(400).send({
    errors: [{ message: "Something went wrong" }],
  });
};
Enter fullscreen mode Exit fullscreen mode

You can extract the validation logic from the controller into a reusable middleware for better reusability.


Running the Server

In your package.json, add a start script:

"scripts": {
  "start": "ts-node-dev src/app.ts"
}
Enter fullscreen mode Exit fullscreen mode

Then run:

npm start
Enter fullscreen mode Exit fullscreen mode

Your server will start at http://localhost:3000.

Use tools like Postman or Insomnia to test your API.


Final Note

This is my first article here. I would love your feedback!
Feel free to leave comments or suggestions for improvements.


Top comments (0)