'use strict'; const common = require('../common'); const assert = require('assert'); const domain = require('domain'); const asyncTest = (function() { let asyncTestsEnabled = false; let asyncTestLastCheck; const asyncTestQueue = []; let asyncTestHandle; let currentTest = null; function fail(error) { const 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() { let 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) { const test = asyncTestQueue.shift(); currentTest = test; test.action(done); } else { clearInterval(asyncTestHandle); } } return function asyncTest(description, fn) { const 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() { const now = Date.now(); if (now - asyncTestLastCheck > 10000) { return fail(new Error('Async test timeout exceeded')); } }, 10); setTimeout(nextAsyncTest, 10); } }; })(); function setupException(fn) { const 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) { const e = new Error(); onUnhandledSucceed(done, function(reason, promise) { assert.strictEqual(e, reason); }); Promise.reject(e); }); asyncTest('synchronously rejected promise should trigger' + ' unhandledRejection', function(done) { const 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) { const 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) { const 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) { const e = new Error(); onUnhandledSucceed(done, function(reason, promise) { assert.strictEqual(e, reason); }); let _reject; const promise = new Promise(function(_, reject) { _reject = reject; }); _reject(e); setImmediate(function() { promise.then(common.fail, function() {}); }); }); asyncTest('When re-throwing new errors in a promise catch, only the' + ' re-thrown error should hit unhandledRejection', function(done) { const e = new Error(); const e2 = new Error(); onUnhandledSucceed(done, function(reason, promise) { assert.strictEqual(e2, reason); assert.strictEqual(promise2, promise); }); let promise2 = Promise.reject(e).then(common.fail, function(reason) { assert.strictEqual(e, reason); throw e2; }); }); asyncTest('Test params of unhandledRejection for a synchronously-rejected' + 'promise', function(done) { const e = new Error(); onUnhandledSucceed(done, function(reason, promise) { assert.strictEqual(e, reason); assert.strictEqual(promise, 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) { const e = new Error(); const e2 = new Error(); onUnhandledSucceed(done, function(reason, promise) { assert.strictEqual(e2, reason); assert.strictEqual(promise2, promise); }); let promise2 = new Promise(function(_, reject) { setTimeout(function() { reject(e); }, 1); }).then(common.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) { const e = new Error(); const e2 = new Error(); onUnhandledSucceed(done, function(reason, promise) { assert.strictEqual(e2, reason); assert.strictEqual(promise2, promise); }); const promise = new Promise(function(_, reject) { setTimeout(function() { reject(e); process.nextTick(function() { promise2 = promise.then(common.fail, function(reason) { assert.strictEqual(e, reason); throw e2; }); }); }, 1); }); let promise2; }); asyncTest( 'unhandledRejection should not be triggered if a promise catch is' + ' attached synchronously upon the promise\'s creation', function(done) { const e = new Error(); onUnhandledFail(done); Promise.reject(e).then(common.fail, function() {}); } ); asyncTest( 'unhandledRejection should not be triggered if a promise catch is' + ' attached synchronously upon the promise\'s creation', function(done) { const e = new Error(); onUnhandledFail(done); new Promise(function(_, reject) { reject(e); }).then(common.fail, function() {}); } ); asyncTest('Attaching a promise catch in a process.nextTick is soon enough to' + ' prevent unhandledRejection', function(done) { const e = new Error(); onUnhandledFail(done); const promise = Promise.reject(e); process.nextTick(function() { promise.then(common.fail, function() {}); }); }); asyncTest('Attaching a promise catch in a process.nextTick is soon enough to' + ' prevent unhandledRejection', function(done) { const e = new Error(); onUnhandledFail(done); const promise = new Promise(function(_, reject) { reject(e); }); process.nextTick(function() { promise.then(common.fail, function() {}); }); }); asyncTest('While inside setImmediate, catching a rejected promise derived ' + 'from returning a rejected promise in a fulfillment handler ' + 'prevents unhandledRejection', function(done) { onUnhandledFail(done); setImmediate(function() { // reproduces on first tick and inside of setImmediate Promise .resolve('resolve') .then(function() { return Promise.reject('reject'); }).catch(function(e) {}); }); }); // State adapation tests asyncTest('catching a promise which is asynchronously rejected (via' + 'resolution to an asynchronously-rejected promise) prevents' + ' unhandledRejection', function(done) { const e = new Error(); onUnhandledFail(done); Promise.resolve().then(function() { return new Promise(function(_, reject) { setTimeout(function() { reject(e); }, 1); }); }).then(common.fail, function(reason) { assert.strictEqual(e, reason); }); }); asyncTest('Catching a rejected promise derived from throwing in a' + ' fulfillment handler prevents unhandledRejection', function(done) { const e = new Error(); onUnhandledFail(done); Promise.resolve().then(function() { throw e; }).then(common.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) { const e = new Error(); onUnhandledFail(done); Promise.resolve().then(function() { return Promise.reject(e); }).then(common.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) { const e = new Error(); let _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) { const e = new Error(); let _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) { const e = new Error(); let _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) { const e = new Error(); onUnhandledFail(done); Promise.all([Promise.reject(e)]).then(common.fail, function() {}); }); asyncTest( 'Catching the Promise.all() of a collection that includes a ' + 'nextTick-async rejected promise prevents unhandledRejection', function(done) { const e = new Error(); onUnhandledFail(done); let p = new Promise(function(_, reject) { process.nextTick(function() { reject(e); }); }); p = Promise.all([p]); process.nextTick(function() { p.then(common.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) { const e = new Error(); onUnhandledSucceed(done, function(reason, promise) { assert.strictEqual(e, reason); assert.strictEqual(p, promise); }); let p = Promise.all([Promise.reject(e)]); }); asyncTest('Waiting setTimeout(, 10) to catch a promise causes an' + ' unhandledRejection + rejectionHandled pair', function(done) { clean(); const unhandledPromises = []; const 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(); }); let thePromise = new Promise(function() { throw e; }); setTimeout(function() { thePromise.then(common.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) { const e = new Error(); onUnhandledFail(done); const 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) { const e = new Error(); onUnhandledFail(done); setImmediate(function() { const 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) { const e = new Error(); onUnhandledFail(done); setTimeout(function() { const 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) { const e = new Error(); onUnhandledFail(done); const 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) { const e = new Error(); onUnhandledFail(done); setImmediate(function() { const 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) { const e = new Error(); onUnhandledFail(done); setTimeout(function() { const 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) { const e = new Error(); onUnhandledSucceed(done, function(reason, promise) { assert.strictEqual(e, reason); assert.strictEqual(p, promise); }); let 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() {}); }); }); }); }); }); let 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); }); let 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) { const d = domain.create(); let domainReceivedError; d.on('error', function(e) { domainReceivedError = e; }); d.run(function() { const e = new Error('error'); const domainError = new Error('domain error'); onUnhandledSucceed(done, function(reason, promise) { assert.strictEqual(reason, e); assert.strictEqual(domainReceivedError, domainError); d.dispose(); }); Promise.reject(e); process.nextTick(function() { throw domainError; }); }); } ); asyncTest('nextTick is immediately scheduled when called inside an event' + ' handler', function(done) { clean(); const e = new Error('error'); process.on('unhandledRejection', function(reason, promise) { const order = []; process.nextTick(function() { order.push(1); }); setTimeout(function() { order.push(2); assert.deepStrictEqual([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(); const e = new Error(); const e2 = new Error(); const tearDownException = setupException(function(err) { assert.strictEqual(e2, err); tearDownException(); done(); }); process.on('rejectionHandled', function() { throw e2; }); const p = Promise.reject(e); setTimeout(function() { try { p.catch(function() {}); } catch (e) { done(new Error('fail')); } }, 1); });