4

Using Node.js + Express (4) + Mongoose (using promises rather than callbacks), I can’t sort out how to tidy up my error handling.

What I've got (rather simplified) is:

app.get('/xxx/:id', function(request, response) {
    Xxx.findById(request.params.id).exec()
        .then(function(xxx) {
            if (xxx == null) throw Error('Xxx '+request.params.id+' not found');
            response.send('Found xxx '+request.params.id);
        })
        .then(null, function(error) { // promise rejected
            switch (error.name) {
                case 'Error':
                    response.status(404).send(error.message); // xxx not found
                    break;
                case 'CastError':
                    response.status(404).send('Invalid id '+request.params.id);
                    break;
                default:
                    response.status(500).send(error.message);
                    break;
            }
        });
});

Here, in the switch in the ‘promise rejected’ section, the Error is the error I threw myself for a potentially valid id which is not found, the CastError is Cast to ObjectId failed thrown by Mongoose for an invalid id, and the 500 error can for instance be triggered by mistyping throw Error() as throw Err() (causing a ReferenceError: Err is not defined).

But like this, every one of my routes has this great big clumsy switch to handle the different errors.

How can I centralise the error handling? Can the switch be tucked away into some middleware somehow?

(I did hope I could just re-throw using throw error; within the 'promise rejected' block, but I haven’t been able to make it work).

3
  • Can't you just make that error handler function global (it's the same everywhere?), and pass it to .then(null, …) in every route? Commented Oct 20, 2014 at 17:11
  • @Bergi: thank you - I had wondered about a global error handler function, I just somehow felt there should be a more ‘native’ way of doing things. The global error handler function works fine – I would have accepted it if you’d made it an answer! Commented Oct 21, 2014 at 6:38
  • @ChrisV the more 'native' way you speak of is using a promise based router rather than the default one which would let you return Xxx.findById(... and then check for rejections. I think spion wrote one at some point. Commented Oct 21, 2014 at 16:16

2 Answers 2

7

I would create middleware to handle errors. Using next() for 404s. and next(err) for other errors.

app.get('/xxx/:id', function(req, res, next) {
  Xxx.findById(req.params.id).exec()
    .then(function(xxx) {
      if (xxx == null) return next(); // Not found
      return res.send('Found xxx '+request.params.id);
    })
    .then(null, function(err) {
      return next(err);
    });
});

404 handler

app.use(function(req, res) {
  return res.send('404');
});

Error handler

app.use(function(err, req, res) {
  switch (err.name) {
    case 'CastError':
      res.status(400); // Bad Request
      return res.send('400');
    default:
      res.status(500); // Internal server error
      return res.send('500');
  }
});

You can improve upon this more by sending a json response like:

return res.json({
  status: 'OK',
  result: someResult
});

or

return res.json({
  status: 'error',
  message: err
});
Sign up to request clarification or add additional context in comments.

9 Comments

When I add return next(err); as you suggest, it gets to that statement, then just sits there with ‘Waiting for localhost...’. If you’re confident that approach should work, perhaps there’s something else wrong in my code, but I can’t for the life of me see what!
Do you have a route that is after that to handle 404 and did you do use app.get('/xxx/:id', function(req, res, next).
what about unpredicted programming error (like ReferenceError) that may introduce leaking references. In this case, the official doc suggest to restart the process, but this middle you implemented will just let the node keeping alive.
@bpcee good point, more should be done with the middleware to only catch errors expected to possibly occur and throw the rest.
Sadly, if you simply throw an Error in the middleware, the node process will not crash as express wraps each function (req, res, next) {} in a try/catch. Still working on figuring out an elegant solution... Let me know if you have any.
|
0

I recently need to do error handling. So, went through so many resources and finally came up with this. We will be creating a custom Error class and ErrorHandler middleware. Custom Error class is used to dynamically send other details like statusCode along with errMsg. And, the middleware is used to centrally handle all the error at once which basically consoles the complete error and send a error response.

  1. Create a custom Err

    class Err extends Error {
       statusCode = 500;
       name = "InternalError";
       err = "Error";
    
       constructor(message, options = {}) {
         super(message);
    
         for (const [key, value] of Object.entries(options)) {
            this[key] = value;
         }
       }
    }
    

The Err class accepts the following unlike the in-built Error class which only accepts message.

  • message: Whatever you want to show to client
  • options: It can include addtional information related to the error like
    • err (the actual error)
    • name (custom/actual name of error)
    • statusCode (like 400, 404, etc)
  1. Create a middleware ErrorHandler

    const errorHandler = (err, req, res, next) => {
       console.error(err);
    
       let errStatus = err.statusCode || 500; 
       let errMsg = err.message;
    
       //handling some basic mongodb error
       if(err.name === 'CastError') {
         errMsg = `Resource not found. Invalid: ${err.path}`;
         errStatus = 400;
       } else if(err.name === 'ValidationError') {
         errMsg = `Invalid input entered: ${Object.values(err.errors).map(e => e.message)}`;
         errStatus = 400;
       } else if(err.code === 11000) {
         errMsg = `Duplicate ${Object.keys(err.keyValues)} entered`;
         errStatus = 403;
       }
       //you can handle many more such in-built or basic errors like related to jwt, etc
    
       return res.status(errStatus).json({
         success: false,
         status: errStatus,
         message: errMsg,
         stack: process.env.ENV === 'DEV' ? err.stack : {}
       })
    }
    

Now, the error handling is just easy-peasy.

  • Whenever we want to throw an custom error we can do like

     const err = throw new Err("Leaderboard not exist for this quiz", {
        err: "RedisError", 
        name: "EmptySetError", 
        statusCode: 422
     });
    

    [Note: It is not required to send all the options, if you want you can only send statusCode.]

  • Or if we are catching an error from try...catch block

     try {
       //do your work like await call
     } catch (err) {
    
     }
    

And we can modify the controller like

const ctrl = (req, res, next) => {
   //err is either custom or we are catching from try...catch
   next(err);
}

We will add this middleware at the last of the all routes in your index.js file.

app.routes....
app.use(ErrorHandler);

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.