node/test/parallel/test-promises-unhandled-rejections.js
Antoine du Hamel e50cbc1abd
test: enforce better never-settling-promise detection
Tests should be explicit regarding whether a promise is expected to
settle, and the test should fail when the behavior does not meet
expectations.

PR-URL: https://github.com/nodejs/node/pull/60976
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
Reviewed-By: Erick Wendel <erick.workspace@gmail.com>
2025-12-10 23:55:36 +00:00

716 lines
21 KiB
JavaScript

// Flags: --unhandled-rejections=none
'use strict';
const common = require('../common');
const assert = require('assert');
const { inspect } = require('util');
const test = (function() {
let asyncTestsEnabled = false;
let asyncTestLastCheck;
const asyncTestQueue = [];
let asyncTestHandle;
let currentTest = null;
function fail(error) {
const stack = currentTest ?
`${inspect(error)}\nFrom previous event:\n${currentTest.stack}` :
inspect(error);
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 = inspect(new Error()).split('\n').slice(1).join('\n');
asyncTestQueue.push({
action: fn,
stack,
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);
}
test('synchronously rejected promise should trigger' +
' unhandledRejection', function(done) {
const e = new Error();
onUnhandledSucceed(done, common.mustCall((reason, promise) => {
assert.strictEqual(reason, e);
}));
Promise.reject(e);
});
test('synchronously rejected promise should trigger' +
' unhandledRejection', function(done) {
const e = new Error();
onUnhandledSucceed(done, common.mustCall((reason, promise) => {
assert.strictEqual(reason, e);
}));
new Promise(function(_, reject) {
reject(e);
});
});
test('Promise rejected after setImmediate should trigger' +
' unhandledRejection', function(done) {
const e = new Error();
onUnhandledSucceed(done, common.mustCall((reason, promise) => {
assert.strictEqual(reason, e);
}));
new Promise(function(_, reject) {
setImmediate(function() {
reject(e);
});
});
});
test('Promise rejected after setTimeout(,1) should trigger' +
' unhandled rejection', function(done) {
const e = new Error();
onUnhandledSucceed(done, common.mustCall((reason, promise) => {
assert.strictEqual(reason, e);
}));
new Promise(function(_, reject) {
setTimeout(function() {
reject(e);
}, 1);
});
});
test('Catching a promise rejection after setImmediate is not' +
' soon enough to stop unhandledRejection', function(done) {
const e = new Error();
onUnhandledSucceed(done, common.mustCall((reason, promise) => {
assert.strictEqual(reason, e);
}));
let _reject;
const promise = new Promise(function(_, reject) {
_reject = reject;
});
_reject(e);
setImmediate(function() {
// eslint-disable-next-line node-core/must-call-assert
promise.then(assert.fail, function() {});
});
});
test('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, common.mustCall((reason, promise) => {
assert.strictEqual(reason, e2);
assert.strictEqual(promise, promise2);
}));
const promise2 = Promise.reject(e).then(assert.fail, function(reason) {
assert.strictEqual(reason, e);
throw e2;
});
});
test('Test params of unhandledRejection for a synchronously-rejected ' +
'promise', function(done) {
const e = new Error();
onUnhandledSucceed(done, common.mustCall((reason, promise) => {
assert.strictEqual(reason, e);
assert.strictEqual(promise, promise);
}));
Promise.reject(e);
});
test('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, common.mustCall((reason, promise) => {
assert.strictEqual(reason, e2);
assert.strictEqual(promise, promise2);
}));
const promise2 = new Promise(function(_, reject) {
setTimeout(function() {
reject(e);
}, 1);
}).then(assert.fail, function(reason) {
assert.strictEqual(reason, e);
throw e2;
});
});
test('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, common.mustCall((reason, promise) => {
assert.strictEqual(reason, e2);
assert.strictEqual(promise, promise2);
}));
const promise = new Promise(function(_, reject) {
setTimeout(function() {
reject(e);
process.nextTick(function() {
promise2 = promise.then(assert.fail, function(reason) {
assert.strictEqual(reason, e);
throw e2;
});
});
}, 1);
});
let promise2;
});
test(
'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);
// eslint-disable-next-line node-core/must-call-assert
Promise.reject(e).then(assert.fail, function() {});
}
);
test(
'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);
// eslint-disable-next-line node-core/must-call-assert
new Promise(function(_, reject) {
reject(e);
}).then(assert.fail, function() {});
}
);
test('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() {
// eslint-disable-next-line node-core/must-call-assert
promise.then(assert.fail, function() {});
});
});
test('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() {
// eslint-disable-next-line node-core/must-call-assert
promise.then(assert.fail, function() {});
});
});
test('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 adaptation tests
test('catching a promise which is asynchronously rejected (via ' +
'resolution to an asynchronously-rejected promise) prevents' +
' unhandledRejection', function(done) {
const e = new Error();
onUnhandledFail(done);
// eslint-disable-next-line node-core/must-call-assert
Promise.resolve().then(function() {
return new Promise(function(_, reject) {
setTimeout(function() {
reject(e);
}, 1);
});
}).then(assert.fail, function(reason) {
assert.strictEqual(reason, e);
});
});
test('Catching a rejected promise derived from throwing in a' +
' fulfillment handler prevents unhandledRejection', function(done) {
const e = new Error();
onUnhandledFail(done);
// eslint-disable-next-line node-core/must-call-assert
Promise.resolve().then(function() {
throw e;
}).then(assert.fail, function(reason) {
assert.strictEqual(reason, e);
});
});
test('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);
// eslint-disable-next-line node-core/must-call-assert
Promise.resolve().then(function() {
return Promise.reject(e);
}).then(assert.fail, function(reason) {
assert.strictEqual(reason, e);
});
});
test('A rejected promise derived from returning an' +
' asynchronously-rejected promise in a fulfillment handler' +
' does trigger unhandledRejection', function(done) {
const e = new Error();
onUnhandledSucceed(done, common.mustCall((reason, promise) => {
assert.strictEqual(reason, e);
assert.strictEqual(promise, _promise);
}));
const _promise = Promise.resolve().then(function() {
return new Promise(function(_, reject) {
setTimeout(function() {
reject(e);
}, 1);
});
});
});
test('A rejected promise derived from throwing in a fulfillment handler' +
' does trigger unhandledRejection', function(done) {
const e = new Error();
onUnhandledSucceed(done, common.mustCall((reason, promise) => {
assert.strictEqual(reason, e);
assert.strictEqual(promise, _promise);
}));
const _promise = Promise.resolve().then(function() {
throw e;
});
});
test(
'A rejected promise derived from returning a synchronously-rejected' +
' promise in a fulfillment handler does trigger unhandledRejection',
function(done) {
const e = new Error();
onUnhandledSucceed(done, common.mustCall((reason, promise) => {
assert.strictEqual(reason, e);
assert.strictEqual(promise, _promise);
}));
const _promise = Promise.resolve().then(function() {
return Promise.reject(e);
});
}
);
// Combinations with Promise.all
test('Catching the Promise.all() of a collection that includes a ' +
'rejected promise prevents unhandledRejection', function(done) {
const e = new Error();
onUnhandledFail(done);
// eslint-disable-next-line node-core/must-call-assert
Promise.all([Promise.reject(e)]).then(assert.fail, function() {});
});
test(
'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() {
// eslint-disable-next-line node-core/must-call-assert
p.then(assert.fail, function() {});
});
}
);
test('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, common.mustCall((reason, promise) => {
assert.strictEqual(reason, e);
assert.strictEqual(promise, p);
}));
const p = Promise.all([Promise.reject(e)]);
});
test('Waiting setTimeout(, 10) to catch a promise causes an' +
' unhandledRejection + rejectionHandled pair', function(done) {
clean();
const unhandledPromises = [];
const e = new Error();
process.on('unhandledRejection', common.mustCall((reason, promise) => {
assert.strictEqual(reason, e);
unhandledPromises.push(promise);
}));
process.on('rejectionHandled', common.mustCall((promise) => {
assert.strictEqual(unhandledPromises.length, 1);
assert.strictEqual(unhandledPromises[0], promise);
assert.strictEqual(promise, thePromise);
done();
}));
const thePromise = new Promise(function() {
throw e;
});
setTimeout(function() {
// eslint-disable-next-line node-core/must-call-assert
thePromise.then(assert.fail, function(reason) {
assert.strictEqual(reason, e);
});
}, 10);
});
test('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(common.mustCall(() => {
process.nextTick(function() {
Promise.resolve().then(common.mustCall(() => {
a.catch(function() {});
}));
});
}));
});
});
test('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(common.mustCall(() => {
const a = Promise.reject(e);
process.nextTick(function() {
Promise.resolve().then(common.mustCall(() => {
process.nextTick(function() {
Promise.resolve().then(common.mustCall(() => {
a.catch(function() {});
}));
});
}));
});
}));
});
test('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(common.mustCall(() => {
const a = Promise.reject(e);
process.nextTick(function() {
Promise.resolve().then(common.mustCall(() => {
process.nextTick(function() {
Promise.resolve().then(common.mustCall(() => {
a.catch(function() {});
}));
});
}));
});
}), 0);
});
test('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(common.mustCall(() => {
process.nextTick(function() {
Promise.resolve().then(common.mustCall(() => {
process.nextTick(function() {
a.catch(function() {});
});
}));
});
}));
});
test(
'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(common.mustCall(() => {
const a = Promise.reject(e);
Promise.resolve().then(common.mustCall(() => {
process.nextTick(function() {
Promise.resolve().then(common.mustCall(() => {
process.nextTick(function() {
a.catch(function() {});
});
}));
});
}));
}));
}
);
test('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(common.mustCall(() => {
const a = Promise.reject(e);
Promise.resolve().then(common.mustCall(() => {
process.nextTick(function() {
Promise.resolve().then(common.mustCall(() => {
process.nextTick(function() {
a.catch(function() {});
});
}));
});
}));
}), 0);
});
test('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, common.mustCall((reason, promise) => {
assert.strictEqual(reason, e);
assert.strictEqual(promise, p);
}));
const p = Promise.reject(e);
setImmediate(common.mustCall(() => {
Promise.resolve().then(common.mustCall(() => {
p.catch(function() {});
}));
}));
});
test('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, common.mustCall((reason, promise) => {
assert.strictEqual(reason, undefined);
assert.strictEqual(promise, p);
}));
setImmediate(common.mustCall(() => {
Promise.resolve().then(common.mustCall(() => {
Promise.resolve().then(common.mustCall(() => {
Promise.resolve().then(common.mustCall(() => {
Promise.resolve().then(common.mustCall(() => {
p.catch(function() {});
}));
}));
}));
}));
}));
const p = Promise.reject();
});
test('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, common.mustCall((reason, promise) => {
assert.strictEqual(reason, undefined);
assert.strictEqual(promise, p);
}));
const p = Promise.reject();
setImmediate(common.mustCall(() => {
Promise.resolve().then(common.mustCall(() => {
Promise.resolve().then(common.mustCall(() => {
Promise.resolve().then(common.mustCall(() => {
Promise.resolve().then(common.mustCall(() => {
p.catch(function() {});
}));
}));
}));
}));
}));
});
test('nextTick is immediately scheduled when called inside an event' +
' handler', function(done) {
clean();
const e = new Error('error');
process.on('unhandledRejection', common.mustCall((reason, promise) => {
const order = [];
process.nextTick(function() {
order.push(1);
});
setTimeout(common.mustCall(() => {
order.push(2);
assert.deepStrictEqual([1, 2], order);
done();
}), 1);
}));
Promise.reject(e);
});
test('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(common.mustCall((err) => {
assert.strictEqual(err, e2);
tearDownException();
done();
}));
process.on('rejectionHandled', function() {
throw e2;
});
const p = Promise.reject(e);
setTimeout(function() {
try {
p.catch(function() {});
} catch {
done(new Error('fail'));
}
}, 1);
});
test('Rejected promise inside unhandledRejection allows nextTick loop' +
' to proceed first', function(done) {
clean();
Promise.reject(0);
let didCall = false;
process.on('unhandledRejection', common.mustCall(() => {
assert(!didCall);
didCall = true;
const promise = Promise.reject(0);
process.nextTick(() => promise.catch(() => done()));
}));
});
test(
'Promise rejection triggers unhandledRejection immediately',
function(done) {
clean();
Promise.reject(0);
process.on('unhandledRejection', common.mustCall((err) => {
if (timer) {
clearTimeout(timer);
timer = null;
done();
}
}));
let timer = setTimeout(common.mustNotCall(), 10000);
},
);
// https://github.com/nodejs/node/issues/30953
test(
'Catching a promise should not take effect on previous promises',
function(done) {
onUnhandledSucceed(done, common.mustCall((reason, promise) => {
assert.strictEqual(reason, '1');
}));
Promise.reject('1');
Promise.reject('2').catch(function() {});
}
);