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)