node/test/parallel/test-promises-unhandled-rejections.js
Petka Antonov 872702d9b7 node: implement unhandled rejection tracking
Implement unhandled rejection tracking for promises as
specified in https://gist.github.com/benjamingr/0237932cee84712951a2

Fixes #256
PR-URL: https://github.com/iojs/io.js/pull/758
Reviewed-By: Trevor Norris <trev.norris@gmail.com>
Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl>
Reviewed-By: Domenic Denicola <domenic@domenicdenicola.com>
2015-02-24 18:27:14 -06:00

606 lines
18 KiB
JavaScript
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

var common = require('../common');
var assert = require('assert');
var domain = require('domain');
var asyncTest = (function() {
var asyncTestsEnabled = false;
var asyncTestLastCheck;
var asyncTestQueue = [];
var asyncTestHandle;
var currentTest = null;
function fail(error) {
var stack = currentTest
? error.stack + '\nFrom previous event:\n' + currentTest.stack
: error.stack;
if (currentTest)
process.stderr.write('\'' + currentTest.description + '\' failed\n\n');
process.stderr.write(stack);
process.exit(2);
}
function nextAsyncTest() {
var called = false;
function done(err) {
if (called) return fail(new Error('done called twice'));
called = true;
asyncTestLastCheck = Date.now();
if (arguments.length > 0) return fail(err);
setTimeout(nextAsyncTest, 10);
}
if (asyncTestQueue.length) {
var test = asyncTestQueue.shift();
currentTest = test;
test.action(done);
} else {
clearInterval(asyncTestHandle);
}
}
return function asyncTest(description, fn) {
var stack = new Error().stack.split('\n').slice(1).join('\n');
asyncTestQueue.push({
action: fn,
stack: stack,
description: description
});
if (!asyncTestsEnabled) {
asyncTestsEnabled = true;
asyncTestLastCheck = Date.now();
process.on('uncaughtException', fail);
asyncTestHandle = setInterval(function() {
var now = Date.now();
if (now - asyncTestLastCheck > 10000) {
return fail(new Error('Async test timeout exceeded'));
}
}, 10);
setTimeout(nextAsyncTest, 10);
}
};
})();
function setupException(fn) {
var listeners = process.listeners('uncaughtException');
process.removeAllListeners('uncaughtException');
process.on('uncaughtException', fn);
return function clean() {
process.removeListener('uncaughtException', fn);
listeners.forEach(function(listener) {
process.on('uncaughtException', listener);
});
};
}
function clean() {
process.removeAllListeners('unhandledRejection');
process.removeAllListeners('rejectionHandled');
}
function onUnhandledSucceed(done, predicate) {
clean();
process.on('unhandledRejection', function(reason, promise) {
try {
predicate(reason, promise);
} catch (e) {
return done(e);
}
done();
});
}
function onUnhandledFail(done) {
clean();
process.on('unhandledRejection', function(reason, promise) {
done(new Error('unhandledRejection not supposed to be triggered'));
});
process.on('rejectionHandled', function() {
done(new Error('rejectionHandled not supposed to be triggered'));
});
setTimeout(function() {
done();
}, 10);
}
asyncTest('synchronously rejected promise should trigger unhandledRejection', function(done) {
var e = new Error();
onUnhandledSucceed(done, function(reason, promise) {
assert.strictEqual(e, reason);
});
Promise.reject(e);
});
asyncTest('synchronously rejected promise should trigger unhandledRejection', function(done) {
var e = new Error();
onUnhandledSucceed(done, function(reason, promise) {
assert.strictEqual(e, reason);
});
new Promise(function(_, reject) {
reject(e);
});
});
asyncTest('Promise rejected after setImmediate should trigger unhandledRejection', function(done) {
var e = new Error();
onUnhandledSucceed(done, function(reason, promise) {
assert.strictEqual(e, reason);
});
new Promise(function(_, reject) {
setImmediate(function() {
reject(e);
});
});
});
asyncTest('Promise rejected after setTimeout(,1) should trigger unhandled rejection', function(done) {
var e = new Error();
onUnhandledSucceed(done, function(reason, promise) {
assert.strictEqual(e, reason);
});
new Promise(function(_, reject) {
setTimeout(function() {
reject(e);
}, 1);
});
});
asyncTest('Catching a promise rejection after setImmediate is not soon enough to stop unhandledRejection', function(done) {
var e = new Error();
onUnhandledSucceed(done, function(reason, promise) {
assert.strictEqual(e, reason);
});
var _reject;
var promise = new Promise(function(_, reject) {
_reject = reject;
})
_reject(e);
setImmediate(function() {
promise.then(assert.fail, function(){});
});
});
asyncTest('When re-throwing new errors in a promise catch, only the re-thrown error should hit unhandledRejection', function(done) {
var e = new Error();
var e2 = new Error();
onUnhandledSucceed(done, function(reason, promise) {
assert.strictEqual(e2, reason);
assert.strictEqual(promise2, promise);
});
var promise2 = Promise.reject(e).then(assert.fail, function(reason) {
assert.strictEqual(e, reason);
throw e2;
});
});
asyncTest('Test params of unhandledRejection for a synchronously-rejected promise', function(done) {
var e = new Error();
var e2 = new Error();
onUnhandledSucceed(done, function(reason, promise) {
assert.strictEqual(e, reason);
assert.strictEqual(promise, promise);
});
var promise = Promise.reject(e);
});
asyncTest('When re-throwing new errors in a promise catch, only the re-thrown error should hit unhandledRejection: original promise rejected async with setTimeout(,1)', function(done) {
var e = new Error();
var e2 = new Error();
onUnhandledSucceed(done, function(reason, promise) {
assert.strictEqual(e2, reason);
assert.strictEqual(promise2, promise);
});
var promise2 = new Promise(function(_, reject) {
setTimeout(function() {
reject(e);
}, 1);
}).then(assert.fail, function(reason) {
assert.strictEqual(e, reason);
throw e2;
});
});
asyncTest('When re-throwing new errors in a promise catch, only the re-thrown error should hit unhandledRejection: promise catch attached a process.nextTick after rejection', function(done) {
var e = new Error();
var e2 = new Error();
onUnhandledSucceed(done, function(reason, promise) {
assert.strictEqual(e2, reason);
assert.strictEqual(promise2, promise);
});
var promise = new Promise(function(_, reject) {
setTimeout(function() {
reject(e);
process.nextTick(function() {
promise2 = promise.then(assert.fail, function(reason) {
assert.strictEqual(e, reason);
throw e2;
});
});
}, 1);
});
var promise2;
});
asyncTest('unhandledRejection should not be triggered if a promise catch is attached synchronously upon the promise\'s creation', function(done) {
var e = new Error();
onUnhandledFail(done);
Promise.reject(e).then(assert.fail, function(){});
});
asyncTest('unhandledRejection should not be triggered if a promise catch is attached synchronously upon the promise\'s creation', function(done) {
var e = new Error();
onUnhandledFail(done);
new Promise(function(_, reject) {
reject(e);
}).then(assert.fail, function(){});
});
asyncTest('Attaching a promise catch in a process.nextTick is soon enough to prevent unhandledRejection', function(done) {
var e = new Error();
onUnhandledFail(done);
var promise = Promise.reject(e);
process.nextTick(function() {
promise.then(assert.fail, function(){});
});
});
asyncTest('Attaching a promise catch in a process.nextTick is soon enough to prevent unhandledRejection', function(done) {
var e = new Error();
onUnhandledFail(done);
var promise = new Promise(function(_, reject) {
reject(e);
});
process.nextTick(function() {
promise.then(assert.fail, function(){});
});
});
// State adapation tests
asyncTest('catching a promise which is asynchronously rejected (via resolution to an asynchronously-rejected promise) prevents unhandledRejection', function(done) {
var e = new Error();
onUnhandledFail(done);
Promise.resolve().then(function() {
return new Promise(function(_, reject) {
setTimeout(function() {
reject(e);
}, 1);
});
}).then(assert.fail, function(reason) {
assert.strictEqual(e, reason);
});
});
asyncTest('Catching a rejected promise derived from throwing in a fulfillment handler prevents unhandledRejection', function(done) {
var e = new Error();
onUnhandledFail(done);
Promise.resolve().then(function() {
throw e;
}).then(assert.fail, function(reason) {
assert.strictEqual(e, reason);
});
});
asyncTest('Catching a rejected promise derived from returning a synchronously-rejected promise in a fulfillment handler prevents unhandledRejection', function(done) {
var e = new Error();
onUnhandledFail(done);
Promise.resolve().then(function() {
return Promise.reject(e);
}).then(assert.fail, function(reason) {
assert.strictEqual(e, reason);
});
});
asyncTest('A rejected promise derived from returning an asynchronously-rejected promise in a fulfillment handler does trigger unhandledRejection', function(done) {
var e = new Error();
var _promise;
onUnhandledSucceed(done, function(reason, promise) {
assert.strictEqual(e, reason);
assert.strictEqual(_promise, promise);
});
_promise = Promise.resolve().then(function() {
return new Promise(function(_, reject) {
setTimeout(function() {
reject(e);
}, 1);
});
});
});
asyncTest('A rejected promise derived from throwing in a fulfillment handler does trigger unhandledRejection', function(done) {
var e = new Error();
var _promise;
onUnhandledSucceed(done, function(reason, promise) {
assert.strictEqual(e, reason);
assert.strictEqual(_promise, promise);
});
_promise = Promise.resolve().then(function() {
throw e;
});
});
asyncTest('A rejected promise derived from returning a synchronously-rejected promise in a fulfillment handler does trigger unhandledRejection', function(done) {
var e = new Error();
var _promise;
onUnhandledSucceed(done, function(reason, promise) {
assert.strictEqual(e, reason);
assert.strictEqual(_promise, promise);
});
_promise = Promise.resolve().then(function() {
return Promise.reject(e);
});
});
// Combinations with Promise.all
asyncTest('Catching the Promise.all() of a collection that includes a rejected promise prevents unhandledRejection', function(done) {
var e = new Error();
onUnhandledFail(done);
Promise.all([Promise.reject(e)]).then(assert.fail, function() {});
});
asyncTest('Catching the Promise.all() of a collection that includes a nextTick-async rejected promise prevents unhandledRejection', function(done) {
var e = new Error();
onUnhandledFail(done);
var p = new Promise(function(_, reject) {
process.nextTick(function() {
reject(e);
});
});
p = Promise.all([p]);
process.nextTick(function() {
p.then(assert.fail, function() {});
});
});
asyncTest('Failing to catch the Promise.all() of a collection that includes a rejected promise triggers unhandledRejection for the returned promise, not the passed promise', function(done) {
var e = new Error();
onUnhandledSucceed(done, function(reason, promise) {
assert.strictEqual(e, reason);
assert.strictEqual(p, promise);
});
var p = Promise.all([Promise.reject(e)]);
});
asyncTest('Waiting setTimeout(, 10) to catch a promise causes an unhandledRejection + rejectionHandled pair', function(done) {
clean();
var unhandledPromises = [];
var e = new Error();
process.on('unhandledRejection', function(reason, promise) {
assert.strictEqual(e, reason);
unhandledPromises.push(promise);
});
process.on('rejectionHandled', function(promise) {
assert.strictEqual(1, unhandledPromises.length);
assert.strictEqual(unhandledPromises[0], promise);
assert.strictEqual(thePromise, promise);
done();
});
var thePromise = new Promise(function() {
throw e;
});
setTimeout(function() {
thePromise.then(assert.fail, function(reason) {
assert.strictEqual(e, reason);
});
}, 10);
});
asyncTest('Waiting for some combination of process.nextTick + promise microtasks to attach a catch handler is still soon enough to prevent unhandledRejection', function(done) {
var e = new Error();
onUnhandledFail(done);
var a = Promise.reject(e);
process.nextTick(function() {
Promise.resolve().then(function() {
process.nextTick(function() {
Promise.resolve().then(function() {
a.catch(function() {});
});
});
});
});
});
asyncTest('Waiting for some combination of process.nextTick + promise microtasks to attach a catch handler is still soon enough to prevent unhandledRejection: inside setImmediate', function(done) {
var e = new Error();
onUnhandledFail(done);
setImmediate(function() {
var a = Promise.reject(e);
process.nextTick(function() {
Promise.resolve().then(function() {
process.nextTick(function() {
Promise.resolve().then(function() {
a.catch(function() {});
});
});
});
});
});
});
asyncTest('Waiting for some combination of process.nextTick + promise microtasks to attach a catch handler is still soon enough to prevent unhandledRejection: inside setTimeout', function(done) {
var e = new Error();
onUnhandledFail(done);
setTimeout(function() {
var a = Promise.reject(e);
process.nextTick(function() {
Promise.resolve().then(function() {
process.nextTick(function() {
Promise.resolve().then(function() {
a.catch(function() {});
});
});
});
});
}, 0);
});
asyncTest('Waiting for some combination of promise microtasks + process.nextTick to attach a catch handler is still soon enough to prevent unhandledRejection', function(done) {
var e = new Error();
onUnhandledFail(done);
var a = Promise.reject(e);
Promise.resolve().then(function() {
process.nextTick(function() {
Promise.resolve().then(function() {
process.nextTick(function() {
a.catch(function() {});
});
});
});
});
});
asyncTest('Waiting for some combination of promise microtasks + process.nextTick to attach a catch handler is still soon enough to prevent unhandledRejection: inside setImmediate', function(done) {
var e = new Error();
onUnhandledFail(done);
setImmediate(function() {
var a = Promise.reject(e);
Promise.resolve().then(function() {
process.nextTick(function() {
Promise.resolve().then(function() {
process.nextTick(function() {
a.catch(function() {});
});
});
});
});
});
});
asyncTest('Waiting for some combination of promise microtasks + process.nextTick to attach a catch handler is still soon enough to prevent unhandledRejection: inside setTimeout', function(done) {
var e = new Error();
onUnhandledFail(done);
setTimeout(function() {
var a = Promise.reject(e);
Promise.resolve().then(function() {
process.nextTick(function() {
Promise.resolve().then(function() {
process.nextTick(function() {
a.catch(function() {});
});
});
});
});
}, 0);
});
asyncTest('setImmediate + promise microtasks is too late to attach a catch handler; unhandledRejection will be triggered in that case. (setImmediate before promise creation/rejection)', function(done) {
var e = new Error();
onUnhandledSucceed(done, function(reason, promise) {
assert.strictEqual(e, reason);
assert.strictEqual(p, promise);
});
var p = Promise.reject(e);
setImmediate(function() {
Promise.resolve().then(function () {
p.catch(function(){});
});
});
});
asyncTest('setImmediate + promise microtasks is too late to attach a catch handler; unhandledRejection will be triggered in that case (setImmediate before promise creation/rejection)', function(done) {
onUnhandledSucceed(done, function(reason, promise) {
assert.strictEqual(undefined, reason);
assert.strictEqual(p, promise);
});
setImmediate(function() {
Promise.resolve().then(function () {
Promise.resolve().then(function () {
Promise.resolve().then(function () {
Promise.resolve().then(function () {
p.catch(function(){});
});
});
});
});
});
var p = Promise.reject();
});
asyncTest('setImmediate + promise microtasks is too late to attach a catch handler; unhandledRejection will be triggered in that case (setImmediate after promise creation/rejection)', function(done) {
onUnhandledSucceed(done, function(reason, promise) {
assert.strictEqual(undefined, reason);
assert.strictEqual(p, promise);
});
var p = Promise.reject();
setImmediate(function() {
Promise.resolve().then(function () {
Promise.resolve().then(function () {
Promise.resolve().then(function () {
Promise.resolve().then(function () {
p.catch(function(){});
});
});
});
});
});
});
asyncTest('Promise unhandledRejection handler does not interfere with domain error handlers being given exceptions thrown from nextTick.', function(done) {
var d = domain.create();
var domainReceivedError;
d.on('error', function(e) {
domainReceivedError = e;
});
d.run(function() {
var e = new Error('error');
var domainError = new Error('domain error');
onUnhandledSucceed(done, function(reason, promise) {
assert.strictEqual(reason, e);
assert.strictEqual(domainReceivedError, domainError);
d.dispose();
});
var a = Promise.reject(e);
process.nextTick(function() {
throw domainError;
});
});
});
asyncTest('nextTick is immediately scheduled when called inside an event handler', function(done) {
clean();
var e = new Error('error');
process.on('unhandledRejection', function(reason, promise) {
var order = [];
process.nextTick(function() {
order.push(1);
});
setTimeout(function() {
order.push(2);
assert.deepEqual([1,2], order);
done();
}, 1);
});
Promise.reject(e);
});
asyncTest('Throwing an error inside a rejectionHandled handler goes to unhandledException, and does not cause .catch() to throw an exception', function(done) {
clean();
var e = new Error();
var e2 = new Error();
var tearDownException = setupException(function(err) {
assert.equal(e2, err);
tearDownException();
done();
});
process.on('rejectionHandled', function() {
throw e2;
});
var p = Promise.reject(e);
setTimeout(function() {
try {
p.catch(function(){});
} catch (e) {
done(new Error('fail'));
}
}, 1);
});