1

I'm using the native MongoDB driver for Node.js and I'm trying to find a way to catch exceptions that may happen in the API's callbacks.

I know that best practice would be to wrap the code in a try-catch block, but in my particular case I expose the driver's API to users so I don't have that level of control. Still, I need to catch potential exceptions happening in callbacks in order to send back relevant error messages.

EDIT: further explanations. Think of some "backend-as-a-service" where users can create JS code that is run on the server, in Node. The user code has direct access to a db object (from the driver) and can do whatever it wants with it, like writing the following:

db.collection('test').findOne({ field: 'value' }, function(err, doc){
  throw new Error;
});

I need to somehow catch that but process.on('uncaughtException') is too low level for me. That user code is executed from an API request so I'd like to return the relevant error message in its response.

So far, I've tried to add an error handler (.on('error')) to the db object, but it doesn't catch anything. Any help would be greatly appreciated.

Thanks!

ThomasWeiss
  • 1,292
  • 16
  • 30
  • Are you returning promises from your API, or callbacks? Can you give us some code examples? How do others consume your API? When they do, how do their calls to your API end up as calls to the MongoDB driver API? Show some code and I'll probably be able to tell you how best to transfer errors around how you want. – CatDadCode Jan 08 '15 at 15:45
  • 1
    Done, thanks for having a look! – ThomasWeiss Jan 08 '15 at 15:55

2 Answers2

5

Any errors coming from the MongoDB driver API are passed to the callbacks as the first argument. Wrapping the calls in try/catch won't help you because the errors happen asynchronously. In other words, all your call to the MongoDB API does is begin an asynchronous operation, passing in a callback to be invoked later when the async operation is complete (your database call returns). If an error happens during the async operation, you won't be able to catch it with try/catch. Only the library itself will be able to catch those errors, which is why the library makes sure to pass them to your callbacks as the first argument.

collection.update({ a : 2 }, { $set: { b : 1 } }, function(err, result) {
  if (err) { /* do error handling */ return; }
  /* err is null, continue as normal */
});

If I were writing my own API that wraps some of these calls I'd do it one of three ways.

  1. If the API I'm exposing is a callback-style API just like the MongoDB driver, then I'd do something like this.

    exports.coolFunction = function (callback) {
      collection.update({ a : 2 }, { $set: { b : 1 } }, function (err, result) {
        if (err) { callback(err); return; }
        callback(null, result);
    };
    

    Then someone could consume my API like this:

    var alexLib = require('alexLibExample');
    alexLib.coolFunction(function (err, result) {
      if (err) { /* consumer handles error how they want to in their app */ }
      /* all is good, no errors, continue normally */
    });
    
  2. Or I could make my API a promise-enabled API.

    var Promise = require('bluebird');
    exports.coolFunction = function () {
      return new Promise(function (resolve, reject) {
        collection.update({ a : 2 }, { $set: { b : 1 } }, function (err, result) {
          if (err) { reject(err); return; }
          resolve(result);
        });
      };
    };
    

    Then someone could consume my promise-enabled API like this:

    var alexLib = require('alexLibExample');
    alexLib.coolFunction().then(function (result) {
      /* do stuff with result */
    }).catch(function (err) {
      /* handle error how user chooses in their app */
    });
    
  3. Or I could emit errors as an error event, which seems to be more what you're after.

    var EventEmitter = require('events').EventEmitter;
    exports.eventEmitter = new EventEmitter();
    exports.coolFunction = function (callback) {
      collection.update({ a : 2 }, { $set: { b : 1 } }, function (err, result) {
        if (err) { exports.eventEmitter.emit('error', err); return; }
        callback(result);
      });
    };
    

    Then someone could consume my event-style API like this:

    var alexLib = require('alexLibExample');
    alexLib.eventEmitter.on('error', function (err) {
      /* user handles error how they choose */
    });
    alexLib.coolFunction(function (result) {
      /* do stuff with result */
    });
    

    The event style of API is usually combined with the callback style. Meaning they still pass errors to the callback functions, which is what most users expect when passing in callbacks. Then they also emit an error event as a sort of global error handler the user can subscribe to. I know this is the way Mongoose works. I can catch the errors on the individual API calls or I can setup an error event handler and handle all errors there.

    var EventEmitter = require('events').EventEmitter;
    exports.eventEmitter = new EventEmitter();
    exports.coolFunction = function (callback) {
      collection.update({ a : 2 }, { $set: { b : 1 } }, function (err, result) {
        if (err) { exports.eventEmitter.emit('error', err); callback(err); return; }
        callback(null, result);
      });
    };
    

    Then the user has some flexibility with how they handle errors.

    var alexLib = require('alexLibExample');
    alexLib.eventEmitter.on('error', function (err) {
      /* handle all errors here */
    });
    alexLib.coolFunction(function (err, result) {
      if (err) { return; }
      /* do stuff with result */
    });
    alexLib.coolFunction2(function (err, result) {
      if (err) { /* maybe I have special error handling for this specific call. I can do that here */ }
      /* do stuff with result */
    });
    

If you really want to get fancy, you can combine all three styles.

var EventEmitter = require('events').EventEmitter;
var Promise = require('bluebird');
exports.eventEmitter = new EventEmitter();
exports.coolFunction = function (callback) {
  return new Promise(function (resolve, reject) {
    collection.update({ a : 2 }, { $set: { b : 1 } }, function(err, result) {
      if (err) {
        if (callback) { callback(err); }
        reject(err);
        exports.eventEmitter.emit('error', err);
      }
      if (callback) { callback(err, result); }
      resolve(result);
    });
  });
};

Then someone could consume my API however they choose.

var alexLib = require('alexLibExample');
alexLib.eventEmitter.on('error', function (err) {
  /* handle all errors here */
});
alexLib.coolFunction(function (err, result) {
  if (err) {
    /* handle this specific error how user chooses */
  }
  /* do stuff with result */
});

// or

alexLib.coolFunction().then(function (result) {
  /* do stuff with result */
}).catch(function (err) {
  /* handle any errors in this promise chain */
});
CatDadCode
  • 58,507
  • 61
  • 212
  • 318
  • 1
    Thanks for this very extensive answer, but I'm afraid it doesn't address my question. I do not wrap the driver API, I expose it directly to the clients. What I'm looking for is a way to catch exceptions thrown by user code in the driver callbacks. I was expecting db.on('error') to catch these but apparently they don't. – ThomasWeiss Jan 09 '15 at 04:42
1

Add this line to the end,,,

process.on('uncaughtException', function (err) {
    console.log(err);
}); 

Through this, the server will not halt if there are any uncaught exceptions in bottom layers.

  • This is not bullet-proof. There are actually scenarios where the user callback function throws an exception in a portion of the node.js runtime functions where the exception is NOT caught by the 'uncaughtException' handler. As an extremely specific example, the process._tickDomainCallback will exit your running process if an unhandled exception is passed in your user-provided callback function. – cshotton Jul 19 '18 at 15:23