2

In the following code example the function baz() throws a TypeError, when invoked within the fs.open callback inside the new Promise callback the node process exits immediately with a non-zero value and the exception is never caught.

var Promise = require('bluebird');
var fs = require('fs');

function baz() {
  [].toDateString(); // should throw type error
}

function bar() {
  return new Promise((resolve, reject) => {
    fs.open('myfile', 'r', (err, fd) => {
      baz();  // throws TypeError [].toDateString which is never caught.
      return resolve('done');
    });
  })
  .catch((error) => {
    console.log('caught errror');
    console.log(error);
    return Promise.resolve(error);
  });
}

bar()
  .then((ret) => {
    console.log('done');
  });

Output:

 $ node promise_test.js
 /Users/.../promise_test.js:5
 [].toDateString(); // should throw type error
    ^

 TypeError: [].toDateString is not a function
   at baz (/Users/..../promise_test.js:5:6)
   at fs.open (/Users/..../promise_test.js:12:7)
   at FSReqWrap.oncomplete (fs.js:123:15)
✘-1 ~/

If I modify this code slightly to throw the exception in the promise callback but outside of the fs.open callback the exception is caught as expected and execution continues:

return new Promise((resolve, reject) => {
 baz();  // throws TypeError [].toDateString
 fs.open('myfile', 'r', (err, fd) => {
   console.log(err);
   return resolve('done');
 });

Output:

$ node promise_test.js
  caught errror
  TypeError: [].toDateString is not a function
  ...
  done
Jared S
  • 83
  • 4

2 Answers2

3

Because the exception occurs inside the fs.open() async callback so that exception goes back into the async event handler in fs.open() that called the completion callback where it then disappears and has no chance to be propagated anywhere. Bluebird never has a chance to see it.

This is a textbook example of why you should not mix regular async callback code with promise code. Instead, promisify fs.open() and use the promisified version and then the exception will be caught appropriately by the promise infrastructure.

fs.openAsync = function(fname, mode) {
    return new Promise(function(resolve, reject) {
        fs.open(fname, mode, function(err, fd) {
            if (err) return reject(err);
            resolve(fd);
        });
    });
}

function bar() {
  return fs.openAsync('myfile', 'r').then(function(fd) {
      baz();  // throws TypeError [].toDateString which will now be caught
              // in the .catch() below
  }).catch(function(err) {
    // handle error here
  });
}

Or, in Bluebird, you can use the built-in promisify functions:

const fs = Promise.promisifyAll(require('fs'));

That will automatically create fs.openAsync() which returns a promise and promisified versions of all the other async methods.


FYI, the promise infrastructure can only catch exceptions in callbacks that are called by the promise infrastructure itself. It does that by wrapping the call to each callback in it's own try/catch. As you see in your code, you are using the fs.open() callback directly which has no such chance to get wrapped in such a try/catch handler to catch the exception and turn it into a rejection. So, instead, the usual solution is to create a promisified version of fs.open() that immediately rejects or resolves in the callback and then your custom callback code goes in the .then() handler where the callback is wrapped and exceptions will be caught and automatically turned into rejections.

jfriend00
  • 683,504
  • 96
  • 985
  • 979
  • Thanks for the answer, this clears up my misunderstanding. A short follow on question. Any exception thrown from within the async function(even if it's wrapped in a promise) will not be caught in the .catch() function? eg. If I invoke baz() within the promisified fs.open it will also not be caught? – Jared S Oct 02 '16 at 23:17
  • @JaredS - In the code I show, an exception in `baz()` will be caught and turned into a rejection which will show in the `.catch()` in my answer. An exception inside a `.then()` handler will be caught by that `.then()` and will turn the current promise chain into a rejected promise which will skip any subsequent `.then()` handlers and will call the next `.catch()` handler. – jfriend00 Oct 03 '16 at 01:23
  • I had to use: `var fs = Promise.promisifyAll(require('fs'));` with Bluebird 3.5.1. – Corey S. Oct 17 '17 at 01:30
  • 1
    @CoreyS. - Yes, that was a typo in my answer which I have corrected. – jfriend00 Oct 17 '17 at 02:42
  • @jfriend00 Thanks! I almost edited, but I wasn't sure if that was syntax from an older version or not. – Corey S. Oct 17 '17 at 23:06
0

This article seems to provide some guidance on promises swallowing exceptions, and how BlueBird can help in handling them:

Promise.onPossiblyUnhandledRejection(function(error){
    throw error;
});

And on the odd chance you do want to discard a rejection, just handle it with an empty catch, like so:

Promise.reject('error value').catch(function() {});

http://jamesknelson.com/are-es6-promises-swallowing-your-errors/

Chad McGrath
  • 1,561
  • 1
  • 11
  • 17
  • Thanks for your response but this isn't an unhandled rejection, it's an exception being thrown and not caught. I think a more relevant document is this: http://bluebirdjs.com/docs/api/catch.html which states: '.catch is a convenience method for handling errors in promise chains. It comes in two variants - A catch-all variant similar to the synchronous catch(e) { block.' My question asks why the exception isn't caught. – Jared S Oct 02 '16 at 20:39
  • @JaredS because it's not in a promise chain – Bergi Oct 02 '16 at 21:36