inversify / inversify-express-utils

Some utilities for the development of Express application with InversifyJS
MIT License
579 stars 98 forks source link

Per-controller error handling #335

Open avishayg opened 3 years ago

avishayg commented 3 years ago

I couldn't find a way to handle all errors in a controller in one handler. For example:

@controller('/user')
export default class UserController implements interfaces.Controller {
    @httpGet('/')
    private async get(@request() req: Request, @response() res: Response) {
        throw new Error('This should be handled in the error handler')
    }

    @httpPost('/')
    private async create(@request() req: Request, @response() res: Response) {
        throw new Error('This should be handled in the error handler')
    }

    // Ideally I would want something like
    private errorHandler(err, req, res) {
        // handle any error in the controller here
    }
}

It's not working also in application level,

app.use(function (err, req, res, next) {
  // errors in the controllers doesn't trigger this
});

Is there an elegant way to handle errors without repeating the code? (If I want for example to log any error to some logger)?

FilipeVeber commented 2 years ago

I was facing this same issue. Here's how we managed that:

1 - Create a handler file with the HTTP responses:

import { NextFunction, Request, Response } from 'express'
import * as HttpStatus from 'http-status'
...

class Handlers {
  public onSuccess(res: Response, data: any): Response {
    return res.status(HttpStatus.OK).json(data)
  }

  public onConflict(res: Response, message: string, err: any): Response {
    return res.status(HttpStatus.CONFLICT).json({ message })
  }

  // Other HTTP responses here
}

2 - Create a middleware to bind in the setup and to call the Handler class

import { NextFunction, Request, Response } from "express";
import Handlers from "../handlers"; // The handler you created
...

export default function errorHandlerMiddleware(err: Error, req: Request, res: Response, next: NextFunction): any {
  if (err instanceof RecordNotFoundError) {
    return Handlers.onNotFound(res, err.message);
  }

  if (err instanceof ConflictError) {
    return Handlers.onConflict(res, err.message, err);
  }

  // If no critearia is matched, return a 500 error
  return Handlers.onError(res, err.message, err);
}

3 - Config your app to bind the middleware

const app = new InversifyExpressServer(container);
app.setConfig(app => {
  // Your configs
  app.use(cors());
  ...
  // bind the middleware
  app.setErrorConfig((app) => {
      app.use(errorHandlerMiddleware);
  });
}

4 - Try/catch your controller and use the 'next' function in the catch block. It'll automatically call the middleware as we are using the Chain of Responsability.

import { Response } from "express";
// Import your handler to use the onSuccess method
import Handlers from "../../../core/handlers";

// Import the next function
import { httpGet,  next, response } from "inversify-express-utils";

  @httpGet("/")
  public async getStuff(
    @response() res: Response,
    @next() next: Function
  ): Promise<Response> {
    try {
      const data = await myData();
      return Handlers.onSuccess(res, data);
    } catch (error) {
      next(error);
    }
  }

And if you want to log the errors, you can call your logger (we are using winston) in the Handler just before the return, as below:

  public onConflict(res: Response, message: string, err: any): Response {
    logger.error(`ERRO: ${err.name}, Message: ${err.message} - Parameters: [${err.parameters}] `)
    return res.status(HttpStatus.CONFLICT).json({ message })
  }