15

What is the best way to handle this scenario. I am in a controlled environment and I don't want to crash.

var Promise = require('bluebird');

function getPromise(){
    return new Promise(function(done, reject){
        setTimeout(function(){
                throw new Error("AJAJAJA");
        }, 500);
    });
}

var p = getPromise();
    p.then(function(){
        console.log("Yay");
    }).error(function(e){
        console.log("Rejected",e);
    }).catch(Error, function(e){
        console.log("Error",e);
    }).catch(function(e){
        console.log("Unknown", e);
    });

When throwing from within the setTimeout we will always get:

$ node bluebird.js

c:\blp\rplus\bbcode\scratchboard\bluebird.js:6
                throw new Error("AJAJAJA");
                      ^
Error: AJAJAJA
    at null._onTimeout (c:\blp\rplus\bbcode\scratchboard\bluebird.js:6:23)
    at Timer.listOnTimeout [as ontimeout] (timers.js:110:15)

If the throw occurs before the setTimeout then bluebirds catch will pick it up:

var Promise = require('bluebird');

function getPromise(){

    return new Promise(function(done, reject){
        throw new Error("Oh no!");
        setTimeout(function(){
            console.log("hihihihi")
        }, 500);
    });
}

var p = getPromise();
    p.then(function(){
        console.log("Yay");
    }).error(function(e){
        console.log("Rejected",e);
    }).catch(Error, function(e){
        console.log("Error",e);
    }).catch(function(e){
        console.log("Unknown", e);
    });

Results in:

$ node bluebird.js
Error [Error: Oh no!]

Which is great - but how would one handle a rogue async callback of this nature in node or the browser.

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
j03m
  • 5,195
  • 4
  • 46
  • 50
  • Wrap the settimeout (or asynchronous method) in a promise so it gets handled just like everything else. – Kevin B Aug 05 '14 at 16:28
  • See also [Using Q.promises: how to catch an async throw?](http://stackoverflow.com/q/15504429/1048572) – Bergi Jul 15 '15 at 20:50

3 Answers3

18

Promises are not domains, they will not catch exceptions from asynchronous callbacks. You just can't do that.

Promises do however catch exceptions that are thrown from within a then / catch / Promise constructor callback. So use

function getPromise(){
    return new Promise(function(done, reject){
        setTimeout(done, 500);
    }).then(function() {
        console.log("hihihihi");
        throw new Error("Oh no!");
    });
}

(or just Promise.delay) to get the desired behaviour. Never throw in custom (non-promise) async callbacks, always reject the surrounding promise. Use try-catch if it really needs to be.

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • What if I don't have control over what is happening in the method invoked by setTimeout? Wrap in another promise per @Kevin B's suggestion? – j03m Aug 05 '14 at 18:28
  • No. When it's out of control, then you can't catch the error. File a bug against that library that you need to gain control. – Bergi Aug 05 '14 at 18:36
  • Hmm, checking what mocha is doing under the hood, as seems to handle something like: it("should respond with hello world", function(done) { setTimeout(function(){ throw new Error("ouch"); done(); }, 500); }); very gracefully. But probably not with promises. – j03m Aug 05 '14 at 18:44
  • 1
    In the browser, it might use `window.onerror`, in node it might use domains. – Bergi Aug 05 '14 at 18:47
  • In the runner: process.on('uncaughtException', uncaught); – j03m Aug 05 '14 at 19:02
1

After dealing with the same scenario and needs you are describing, i've discovered zone.js , an amazing javascript library , used in multiple frameworks (Angular is one of them), that allows us to handle those scenarios in a very elegant way.

A Zone is an execution context that persists across async tasks. You can think of it as thread-local storage for JavaScript VMs

Using your example code :

import 'zone.js'

function getPromise(){
  return new Promise(function(done, reject){
    setTimeout(function(){
      throw new Error("AJAJAJA");
    }, 500);
  });
}

Zone.current
  .fork({
    name: 'your-zone-name',
    onHandleError: function(parent, current, target, error) {
      // handle the error 
      console.log(error.message) // --> 'AJAJAJA'
      // and return false to prevent it to be re-thrown
      return false
    }
  })
  .runGuarded(async () => {
    await getPromise()
  })
colxi
  • 7,640
  • 2
  • 45
  • 43
0

Thank @Bergi. Now i know promise does not catch error in async callback. Here is my 3 examples i have tested.

Note: After call reject, function will continue running.

Example 1: reject, then throw error in promise constructor callback

Example 2: reject, then throw error in setTimeout async callback

Example 3: reject, then return in setTimeout async callback to avoid crashing

// Caught
// only error 1 is sent
// error 2 is reached but not send reject again
new Promise((resolve, reject) => {
  reject("error 1"); // Send reject
  console.log("Continue"); // Print 
  throw new Error("error 2"); // Nothing happen
})
  .then(() => {})
  .catch(err => {
    console.log("Error", err);
  });


// Uncaught
// error due to throw new Error() in setTimeout async callback
// solution: return after reject
new Promise((resolve, reject) => {
  setTimeout(() => {
    reject("error 1"); // Send reject
    console.log("Continue"); // Print

    throw new Error("error 2"); // Did run and cause Uncaught error
  }, 0);
})
  .then(data => {})
  .catch(err => {
    console.log("Error", err);
  });


// Caught
// Only error 1 is sent
// error 2 cannot be reached but can cause potential uncaught error if err = null
new Promise((resolve, reject) => {
  setTimeout(() => {
    const err = "error 1";
    if (err) {
      reject(err); // Send reject
      console.log("Continue"); // Did print
      return;
    }
    throw new Error("error 2"); // Potential Uncaught error if err = null
  }, 0);
})
  .then(data => {})
  .catch(err => {
    console.log("Error", err);
  });
Solominh
  • 2,808
  • 1
  • 17
  • 9