简体   繁体   中英

NodeJS best practices for catching errors

I'm starting out w/ NodeJS and Express. Coming from the other popular scripting languages and C++ background, asynchronously calling DB functions is a bit foreign. I've sorted out a pattern, but I'm still curious about catching exceptions. Below is my basic pattern.

var callback = function(req, res) {
    // do stuff
    connection.query(queryString, function(err,result){
        if (err) throw err;
        // process results.
    };
};

var express = require('express');
var app     = express();

app.get('/', callback);
app.listen(3000,function() {
    console.log('listening');
};

Generally I have a lot of endpoints and callbacks. I'm a bit lost on where I set up ta try/catch block to catch errors thrown in the callback though. I've looked around for some suggestions, but a lot of them seem to be on the web framework (if any) being used.

When you throw in an asynchronous callback, the exception just goes back to the internals of the database event handler and there is NO way for you to ever catch or handle that exception. So, basically it does no good at all. It will just cause you to abort the handling of that request and you will never send a response on that request.

Basically, you have several choices for how to handle the error. You can handle it completely right in each endpoint and send some sort of error response.

Send Response right at each point of error

app.get('/', function(req, res) {
    // do stuff
    connection.query(queryString, function(err,result){
        if (err) return res.status(500).send(someErrorResponse);
        // process results.
    };
});

Forward on to centralized error handler

Or, you can forward the error on to a centralized error handler by calling next(err) :

app.get('/', function(req, res, next) {
    // do stuff
    connection.query(queryString, function(err,result){
        // if error, forward it on to our centralized error handler
        if (err) return next(err);
        // process results.
    };
});

// centralized error handler - note how it has four parameters
app.use(function(err, req, res, next) {
    // formulate an error response here
    console.log(err);
    res.status(500).send(someErrorMessage)
});

See Nodejs handle unsupported URLs and request types for more info on the ways to have generalized error handlers in Express.

Use promises to collect errors within each route

If you are using more involved sequences of asynchronous operations where you may have more than one async operation sequenced together, then it does get to be a pain to handle errors at every single async operation. This is where using promises with all your async operations more easily allows all the errors to percolate up to one .catch() statement at the top level of each route. You don't say what database you're using, but here's an idea what that looks like. The general idea is that you can write your code so that all promise rejections (eg errors) will propagate up to one central .catch() in each route handler and you can then call next(err) from that .catch() , sending the error to your centralized error handler. Here's how that looks for a recent version of Mongoose (you didn't say which database you were using) with one database operation.

app.get('/', function(req, res, next) {
    // do stuff
    connection.query(queryString).exec().then(function(result){
        // process results.
    }).catch(next);
});

// centralized error handler - note how it has four parameters
app.use(function(err, req, res, next) {
    // formulate an error response here
    console.log(err);
    res.status(500).send(someErrorMessage)
});

And, here's what it looks like if you have more than one operation:

app.get('/', function(req, res, next) {
    // do stuff
    connection.query(queryString).exec().then(function(result){
        // process results, then make another query
        // return the promise from this second operaton so both results 
        // and error are chained to the first promise
        return connection.query(...).exec();
    }).then(function(result) {
        // process chained result
    }).catch(next);
});

// centralized error handler - note how it has four parameters
app.use(function(err, req, res, next) {
    // formulate an error response here
    console.log(err);
    res.status(500).send(someErrorMessage)
});

Since ES6 built in support for promises and ES7 will add support for async/await for asynchronous operations (which is based on promises) and all significant libraries that offer asynchronous operations have added or are adding support for promises, it is clear that promises are the future of the language for managing asynchronous operations. That would be my strong recommendation.

You should never, ever throw an error like that! :) The reason is that at some point your whole node app will just stop working, because of some db query failed. This should be handled instead of just die.

And because this is a route handler - handles specific url that the user is getting (for example / ), there should be some output. You can always show a page with status 500 and a nice design, if there was such an error that you cannot handle or you might have your internal state messed up.

So basically just act as nothing happened - return respones of any kind, or even render a page, but provide information that something went wrong.

Also, a common scenario is something like what Alon Oz presented. All routes in express are actually a middleware functions, that are called one after another. If the route does not match the requested one, the function just skips and the next one is called. You can manually control that. The actual pattern of the router is this:

app.get('/', function(req, res, next) {
    // you can have the request
    // you can send response like res.send('hello')
    // OR you can skip this function using NEXT
});

The actual signature of next is next(err) . So if you call it without any arguments, it will just skip to the next middleware. If you call it with an argument, it will skip all regular functions and go to the last ones in the stack, or more specifically the ones that handle errors. They are like the regular ones, but taking four arguments instead of three:

app.use(function (err, req, res, next) { });

It's very important to understand that this function will be called if you call next with an argument. Throwing an error won't do any good! Of course if none of your routes match the specific criteria (url) the final one will in the call will be called, so you can still handle the "not found" error.

This is a common scenario that you will use:

// development error handler, will print stacktrace
if (app.get('env') === 'development') {
    app.use(function(err, req, res, next) {
        debug('ERROR [ip: %s]:: dev env -> ', req.ip, err); // I'm using debug library - very helpful
        res.status(err.status || 500);
        res.render('deverr', { // I render custom template with the whole stack beautifully displayed
            errMessage: err.message,
            error: err
        });
    });
}

// production error handler, no stacktraces leaked to user
app.use(function(err, req, res, next) {
    res.status(err.status || 500);
    res.render('pages/error', { // custom error page with nice design and a message
        errMessage: err.message,
        error: {}
    });
});

Hope that helps! :)

Since you are using express, it has its own way to handle exceptions, defined like this:

function clientErrorHandler (err, req, res, next) {
    if (req.xhr) {
        res.status(500).send({ error: 'Something failed!' })
    } else {
        next(err)
    }
}
app.use(clientErrorHandler)

For more info:

https://expressjs.com/en/guide/error-handling.html

There are most commonly three major types of errors that we need to take into account.

  1. Promise failures (Any failures that come up during async/await or result of a promise in then/catch)

In order to handle promise failures, as suggested in the strong loop document or node js 2018 best practices , it's important to have a common function that can handle it.

// app.js file

app.get('/:id', async (req,res,next) => {
    if(!req.params.id) {
         return res.status(412).send('enter a valid user id');
    }
    try {
       const results = await UserDAL(id);
    } catch(e) {
      next(e);
    }
}

// common error middleware defined in middleware/error.js

module.exports = function (err,req,res,next) {
     logger.error(`${err.status || 500} - ${err.message} - ${req.originalUrl} - ${req.method} - ${req.ip}`);
     return res.status(500).send('something failed.');
};
  1. Unhandled Rejections

    process.on('unhandledRejection', e => { // do something });

  2. Unhandled exceptions

    process.on('uncaughtException', e => { // do something });

If you see a lot of try/ catch blocks in your express methods you can abstract that to a separate async function like below:

module.exports = function asyncMiddleWare(handler) {
    return async (req,res,next) => {
        try {
            await handler(req,res)
        } catch(e) {
            next(e);
        }
    }
};

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM