Search code examples
javascriptnode.jspromisebluebird

Why is an error thrown from within an async function inside a bluebird promise not caught in the .catch() function?


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

Solution

  • 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.