1// Flags: --unhandled-rejections=none
2'use strict';
3const common = require('../common');
4const assert = require('assert');
5const { inspect } = require('util');
6
7const asyncTest = (function() {
8  let asyncTestsEnabled = false;
9  let asyncTestLastCheck;
10  const asyncTestQueue = [];
11  let asyncTestHandle;
12  let currentTest = null;
13
14  function fail(error) {
15    const stack = currentTest ?
16      `${inspect(error)}\nFrom previous event:\n${currentTest.stack}` :
17      inspect(error);
18
19    if (currentTest)
20      process.stderr.write(`'${currentTest.description}' failed\n\n`);
21
22    process.stderr.write(stack);
23    process.exit(2);
24  }
25
26  function nextAsyncTest() {
27    let called = false;
28    function done(err) {
29      if (called) return fail(new Error('done called twice'));
30      called = true;
31      asyncTestLastCheck = Date.now();
32      if (arguments.length > 0) return fail(err);
33      setTimeout(nextAsyncTest, 10);
34    }
35
36    if (asyncTestQueue.length) {
37      const test = asyncTestQueue.shift();
38      currentTest = test;
39      test.action(done);
40    } else {
41      clearInterval(asyncTestHandle);
42    }
43  }
44
45  return function asyncTest(description, fn) {
46    const stack = inspect(new Error()).split('\n').slice(1).join('\n');
47    asyncTestQueue.push({
48      action: fn,
49      stack,
50      description
51    });
52    if (!asyncTestsEnabled) {
53      asyncTestsEnabled = true;
54      asyncTestLastCheck = Date.now();
55      process.on('uncaughtException', fail);
56      asyncTestHandle = setInterval(function() {
57        const now = Date.now();
58        if (now - asyncTestLastCheck > 10000) {
59          return fail(new Error('Async test timeout exceeded'));
60        }
61      }, 10);
62      setTimeout(nextAsyncTest, 10);
63    }
64  };
65
66})();
67
68function setupException(fn) {
69  const listeners = process.listeners('uncaughtException');
70  process.removeAllListeners('uncaughtException');
71  process.on('uncaughtException', fn);
72  return function clean() {
73    process.removeListener('uncaughtException', fn);
74    listeners.forEach(function(listener) {
75      process.on('uncaughtException', listener);
76    });
77  };
78}
79
80function clean() {
81  process.removeAllListeners('unhandledRejection');
82  process.removeAllListeners('rejectionHandled');
83}
84
85function onUnhandledSucceed(done, predicate) {
86  clean();
87  process.on('unhandledRejection', function(reason, promise) {
88    try {
89      predicate(reason, promise);
90    } catch (e) {
91      return done(e);
92    }
93    done();
94  });
95}
96
97function onUnhandledFail(done) {
98  clean();
99  process.on('unhandledRejection', function(reason, promise) {
100    done(new Error('unhandledRejection not supposed to be triggered'));
101  });
102  process.on('rejectionHandled', function() {
103    done(new Error('rejectionHandled not supposed to be triggered'));
104  });
105  setTimeout(function() {
106    done();
107  }, 10);
108}
109
110asyncTest('synchronously rejected promise should trigger' +
111          ' unhandledRejection', function(done) {
112  const e = new Error();
113  onUnhandledSucceed(done, function(reason, promise) {
114    assert.strictEqual(reason, e);
115  });
116  Promise.reject(e);
117});
118
119asyncTest('synchronously rejected promise should trigger' +
120          ' unhandledRejection', function(done) {
121  const e = new Error();
122  onUnhandledSucceed(done, function(reason, promise) {
123    assert.strictEqual(reason, e);
124  });
125  new Promise(function(_, reject) {
126    reject(e);
127  });
128});
129
130asyncTest('Promise rejected after setImmediate should trigger' +
131          ' unhandledRejection', function(done) {
132  const e = new Error();
133  onUnhandledSucceed(done, function(reason, promise) {
134    assert.strictEqual(reason, e);
135  });
136  new Promise(function(_, reject) {
137    setImmediate(function() {
138      reject(e);
139    });
140  });
141});
142
143asyncTest('Promise rejected after setTimeout(,1) should trigger' +
144          ' unhandled rejection', function(done) {
145  const e = new Error();
146  onUnhandledSucceed(done, function(reason, promise) {
147    assert.strictEqual(reason, e);
148  });
149  new Promise(function(_, reject) {
150    setTimeout(function() {
151      reject(e);
152    }, 1);
153  });
154});
155
156asyncTest('Catching a promise rejection after setImmediate is not' +
157          ' soon enough to stop unhandledRejection', function(done) {
158  const e = new Error();
159  onUnhandledSucceed(done, function(reason, promise) {
160    assert.strictEqual(reason, e);
161  });
162  let _reject;
163  const promise = new Promise(function(_, reject) {
164    _reject = reject;
165  });
166  _reject(e);
167  setImmediate(function() {
168    promise.then(assert.fail, function() {});
169  });
170});
171
172asyncTest('When re-throwing new errors in a promise catch, only the' +
173          ' re-thrown error should hit unhandledRejection', function(done) {
174  const e = new Error();
175  const e2 = new Error();
176  onUnhandledSucceed(done, function(reason, promise) {
177    assert.strictEqual(reason, e2);
178    assert.strictEqual(promise, promise2);
179  });
180  const promise2 = Promise.reject(e).then(assert.fail, function(reason) {
181    assert.strictEqual(reason, e);
182    throw e2;
183  });
184});
185
186asyncTest('Test params of unhandledRejection for a synchronously-rejected ' +
187          'promise', function(done) {
188  const e = new Error();
189  onUnhandledSucceed(done, function(reason, promise) {
190    assert.strictEqual(reason, e);
191    assert.strictEqual(promise, promise);
192  });
193  Promise.reject(e);
194});
195
196asyncTest('When re-throwing new errors in a promise catch, only the ' +
197          're-thrown error should hit unhandledRejection: original promise' +
198          ' rejected async with setTimeout(,1)', function(done) {
199  const e = new Error();
200  const e2 = new Error();
201  onUnhandledSucceed(done, function(reason, promise) {
202    assert.strictEqual(reason, e2);
203    assert.strictEqual(promise, promise2);
204  });
205  const promise2 = new Promise(function(_, reject) {
206    setTimeout(function() {
207      reject(e);
208    }, 1);
209  }).then(assert.fail, function(reason) {
210    assert.strictEqual(reason, e);
211    throw e2;
212  });
213});
214
215asyncTest('When re-throwing new errors in a promise catch, only the re-thrown' +
216          ' error should hit unhandledRejection: promise catch attached a' +
217          ' process.nextTick after rejection', function(done) {
218  const e = new Error();
219  const e2 = new Error();
220  onUnhandledSucceed(done, function(reason, promise) {
221    assert.strictEqual(reason, e2);
222    assert.strictEqual(promise, promise2);
223  });
224  const promise = new Promise(function(_, reject) {
225    setTimeout(function() {
226      reject(e);
227      process.nextTick(function() {
228        promise2 = promise.then(assert.fail, function(reason) {
229          assert.strictEqual(reason, e);
230          throw e2;
231        });
232      });
233    }, 1);
234  });
235  let promise2;
236});
237
238asyncTest(
239  'unhandledRejection should not be triggered if a promise catch is' +
240  ' attached synchronously upon the promise\'s creation',
241  function(done) {
242    const e = new Error();
243    onUnhandledFail(done);
244    Promise.reject(e).then(assert.fail, function() {});
245  }
246);
247
248asyncTest(
249  'unhandledRejection should not be triggered if a promise catch is' +
250  ' attached synchronously upon the promise\'s creation',
251  function(done) {
252    const e = new Error();
253    onUnhandledFail(done);
254    new Promise(function(_, reject) {
255      reject(e);
256    }).then(assert.fail, function() {});
257  }
258);
259
260asyncTest('Attaching a promise catch in a process.nextTick is soon enough to' +
261          ' prevent unhandledRejection', function(done) {
262  const e = new Error();
263  onUnhandledFail(done);
264  const promise = Promise.reject(e);
265  process.nextTick(function() {
266    promise.then(assert.fail, function() {});
267  });
268});
269
270asyncTest('Attaching a promise catch in a process.nextTick is soon enough to' +
271          ' prevent unhandledRejection', function(done) {
272  const e = new Error();
273  onUnhandledFail(done);
274  const promise = new Promise(function(_, reject) {
275    reject(e);
276  });
277  process.nextTick(function() {
278    promise.then(assert.fail, function() {});
279  });
280});
281
282asyncTest('While inside setImmediate, catching a rejected promise derived ' +
283          'from returning a rejected promise in a fulfillment handler ' +
284          'prevents unhandledRejection', function(done) {
285  onUnhandledFail(done);
286
287  setImmediate(function() {
288    // Reproduces on first tick and inside of setImmediate
289    Promise
290      .resolve('resolve')
291      .then(function() {
292        return Promise.reject('reject');
293      }).catch(function(e) {});
294  });
295});
296
297// State adaptation tests
298asyncTest('catching a promise which is asynchronously rejected (via ' +
299          'resolution to an asynchronously-rejected promise) prevents' +
300          ' unhandledRejection', function(done) {
301  const e = new Error();
302  onUnhandledFail(done);
303  Promise.resolve().then(function() {
304    return new Promise(function(_, reject) {
305      setTimeout(function() {
306        reject(e);
307      }, 1);
308    });
309  }).then(assert.fail, function(reason) {
310    assert.strictEqual(reason, e);
311  });
312});
313
314asyncTest('Catching a rejected promise derived from throwing in a' +
315          ' fulfillment handler prevents unhandledRejection', function(done) {
316  const e = new Error();
317  onUnhandledFail(done);
318  Promise.resolve().then(function() {
319    throw e;
320  }).then(assert.fail, function(reason) {
321    assert.strictEqual(reason, e);
322  });
323});
324
325asyncTest('Catching a rejected promise derived from returning a' +
326          ' synchronously-rejected promise in a fulfillment handler' +
327          ' prevents unhandledRejection', function(done) {
328  const e = new Error();
329  onUnhandledFail(done);
330  Promise.resolve().then(function() {
331    return Promise.reject(e);
332  }).then(assert.fail, function(reason) {
333    assert.strictEqual(reason, e);
334  });
335});
336
337asyncTest('A rejected promise derived from returning an' +
338          ' asynchronously-rejected promise in a fulfillment handler' +
339          ' does trigger unhandledRejection', function(done) {
340  const e = new Error();
341  onUnhandledSucceed(done, function(reason, promise) {
342    assert.strictEqual(reason, e);
343    assert.strictEqual(promise, _promise);
344  });
345  const _promise = Promise.resolve().then(function() {
346    return new Promise(function(_, reject) {
347      setTimeout(function() {
348        reject(e);
349      }, 1);
350    });
351  });
352});
353
354asyncTest('A rejected promise derived from throwing in a fulfillment handler' +
355          ' does trigger unhandledRejection', function(done) {
356  const e = new Error();
357  onUnhandledSucceed(done, function(reason, promise) {
358    assert.strictEqual(reason, e);
359    assert.strictEqual(promise, _promise);
360  });
361  const _promise = Promise.resolve().then(function() {
362    throw e;
363  });
364});
365
366asyncTest(
367  'A rejected promise derived from returning a synchronously-rejected' +
368  ' promise in a fulfillment handler does trigger unhandledRejection',
369  function(done) {
370    const e = new Error();
371    onUnhandledSucceed(done, function(reason, promise) {
372      assert.strictEqual(reason, e);
373      assert.strictEqual(promise, _promise);
374    });
375    const _promise = Promise.resolve().then(function() {
376      return Promise.reject(e);
377    });
378  }
379);
380
381// Combinations with Promise.all
382asyncTest('Catching the Promise.all() of a collection that includes a ' +
383          'rejected promise prevents unhandledRejection', function(done) {
384  const e = new Error();
385  onUnhandledFail(done);
386  Promise.all([Promise.reject(e)]).then(assert.fail, function() {});
387});
388
389asyncTest(
390  'Catching the Promise.all() of a collection that includes a ' +
391  'nextTick-async rejected promise prevents unhandledRejection',
392  function(done) {
393    const e = new Error();
394    onUnhandledFail(done);
395    let p = new Promise(function(_, reject) {
396      process.nextTick(function() {
397        reject(e);
398      });
399    });
400    p = Promise.all([p]);
401    process.nextTick(function() {
402      p.then(assert.fail, function() {});
403    });
404  }
405);
406
407asyncTest('Failing to catch the Promise.all() of a collection that includes' +
408          ' a rejected promise triggers unhandledRejection for the returned' +
409          ' promise, not the passed promise', function(done) {
410  const e = new Error();
411  onUnhandledSucceed(done, function(reason, promise) {
412    assert.strictEqual(reason, e);
413    assert.strictEqual(promise, p);
414  });
415  const p = Promise.all([Promise.reject(e)]);
416});
417
418asyncTest('Waiting setTimeout(, 10) to catch a promise causes an' +
419          ' unhandledRejection + rejectionHandled pair', function(done) {
420  clean();
421  const unhandledPromises = [];
422  const e = new Error();
423  process.on('unhandledRejection', function(reason, promise) {
424    assert.strictEqual(reason, e);
425    unhandledPromises.push(promise);
426  });
427  process.on('rejectionHandled', function(promise) {
428    assert.strictEqual(unhandledPromises.length, 1);
429    assert.strictEqual(unhandledPromises[0], promise);
430    assert.strictEqual(promise, thePromise);
431    done();
432  });
433
434  const thePromise = new Promise(function() {
435    throw e;
436  });
437  setTimeout(function() {
438    thePromise.then(assert.fail, function(reason) {
439      assert.strictEqual(reason, e);
440    });
441  }, 10);
442});
443
444asyncTest('Waiting for some combination of process.nextTick + promise' +
445          ' microtasks to attach a catch handler is still soon enough to' +
446          ' prevent unhandledRejection', function(done) {
447  const e = new Error();
448  onUnhandledFail(done);
449
450
451  const a = Promise.reject(e);
452  process.nextTick(function() {
453    Promise.resolve().then(function() {
454      process.nextTick(function() {
455        Promise.resolve().then(function() {
456          a.catch(function() {});
457        });
458      });
459    });
460  });
461});
462
463asyncTest('Waiting for some combination of process.nextTick + promise' +
464          ' microtasks to attach a catch handler is still soon enough to ' +
465          'prevent unhandledRejection: inside setImmediate', function(done) {
466  const e = new Error();
467  onUnhandledFail(done);
468
469  setImmediate(function() {
470    const a = Promise.reject(e);
471    process.nextTick(function() {
472      Promise.resolve().then(function() {
473        process.nextTick(function() {
474          Promise.resolve().then(function() {
475            a.catch(function() {});
476          });
477        });
478      });
479    });
480  });
481});
482
483asyncTest('Waiting for some combination of process.nextTick + promise ' +
484          'microtasks to attach a catch handler is still soon enough to ' +
485          'prevent unhandledRejection: inside setTimeout', function(done) {
486  const e = new Error();
487  onUnhandledFail(done);
488
489  setTimeout(function() {
490    const a = Promise.reject(e);
491    process.nextTick(function() {
492      Promise.resolve().then(function() {
493        process.nextTick(function() {
494          Promise.resolve().then(function() {
495            a.catch(function() {});
496          });
497        });
498      });
499    });
500  }, 0);
501});
502
503asyncTest('Waiting for some combination of promise microtasks + ' +
504          'process.nextTick to attach a catch handler is still soon enough' +
505          ' to prevent unhandledRejection', function(done) {
506  const e = new Error();
507  onUnhandledFail(done);
508
509
510  const a = Promise.reject(e);
511  Promise.resolve().then(function() {
512    process.nextTick(function() {
513      Promise.resolve().then(function() {
514        process.nextTick(function() {
515          a.catch(function() {});
516        });
517      });
518    });
519  });
520});
521
522asyncTest(
523  'Waiting for some combination of promise microtasks +' +
524  ' process.nextTick to attach a catch handler is still soon enough' +
525  ' to prevent unhandledRejection: inside setImmediate',
526  function(done) {
527    const e = new Error();
528    onUnhandledFail(done);
529
530    setImmediate(function() {
531      const a = Promise.reject(e);
532      Promise.resolve().then(function() {
533        process.nextTick(function() {
534          Promise.resolve().then(function() {
535            process.nextTick(function() {
536              a.catch(function() {});
537            });
538          });
539        });
540      });
541    });
542  }
543);
544
545asyncTest('Waiting for some combination of promise microtasks +' +
546          ' process.nextTick to attach a catch handler is still soon enough' +
547          ' to prevent unhandledRejection: inside setTimeout', function(done) {
548  const e = new Error();
549  onUnhandledFail(done);
550
551  setTimeout(function() {
552    const a = Promise.reject(e);
553    Promise.resolve().then(function() {
554      process.nextTick(function() {
555        Promise.resolve().then(function() {
556          process.nextTick(function() {
557            a.catch(function() {});
558          });
559        });
560      });
561    });
562  }, 0);
563});
564
565asyncTest('setImmediate + promise microtasks is too late to attach a catch' +
566          ' handler; unhandledRejection will be triggered in that case.' +
567          ' (setImmediate before promise creation/rejection)', function(done) {
568  const e = new Error();
569  onUnhandledSucceed(done, function(reason, promise) {
570    assert.strictEqual(reason, e);
571    assert.strictEqual(promise, p);
572  });
573  const p = Promise.reject(e);
574  setImmediate(function() {
575    Promise.resolve().then(function() {
576      p.catch(function() {});
577    });
578  });
579});
580
581asyncTest('setImmediate + promise microtasks is too late to attach a catch' +
582          ' handler; unhandledRejection will be triggered in that case' +
583          ' (setImmediate before promise creation/rejection)', function(done) {
584  onUnhandledSucceed(done, function(reason, promise) {
585    assert.strictEqual(reason, undefined);
586    assert.strictEqual(promise, p);
587  });
588  setImmediate(function() {
589    Promise.resolve().then(function() {
590      Promise.resolve().then(function() {
591        Promise.resolve().then(function() {
592          Promise.resolve().then(function() {
593            p.catch(function() {});
594          });
595        });
596      });
597    });
598  });
599  const p = Promise.reject();
600});
601
602asyncTest('setImmediate + promise microtasks is too late to attach a catch' +
603          ' handler; unhandledRejection will be triggered in that case' +
604          ' (setImmediate after promise creation/rejection)', function(done) {
605  onUnhandledSucceed(done, function(reason, promise) {
606    assert.strictEqual(reason, undefined);
607    assert.strictEqual(promise, p);
608  });
609  const p = Promise.reject();
610  setImmediate(function() {
611    Promise.resolve().then(function() {
612      Promise.resolve().then(function() {
613        Promise.resolve().then(function() {
614          Promise.resolve().then(function() {
615            p.catch(function() {});
616          });
617        });
618      });
619    });
620  });
621});
622
623asyncTest('nextTick is immediately scheduled when called inside an event' +
624          ' handler', function(done) {
625  clean();
626  const e = new Error('error');
627  process.on('unhandledRejection', function(reason, promise) {
628    const order = [];
629    process.nextTick(function() {
630      order.push(1);
631    });
632    setTimeout(function() {
633      order.push(2);
634      assert.deepStrictEqual([1, 2], order);
635      done();
636    }, 1);
637  });
638  Promise.reject(e);
639});
640
641asyncTest('Throwing an error inside a rejectionHandled handler goes to' +
642          ' unhandledException, and does not cause .catch() to throw an ' +
643          'exception', function(done) {
644  clean();
645  const e = new Error();
646  const e2 = new Error();
647  const tearDownException = setupException(function(err) {
648    assert.strictEqual(err, e2);
649    tearDownException();
650    done();
651  });
652  process.on('rejectionHandled', function() {
653    throw e2;
654  });
655  const p = Promise.reject(e);
656  setTimeout(function() {
657    try {
658      p.catch(function() {});
659    } catch {
660      done(new Error('fail'));
661    }
662  }, 1);
663});
664
665asyncTest('Rejected promise inside unhandledRejection allows nextTick loop' +
666          ' to proceed first', function(done) {
667  clean();
668  Promise.reject(0);
669  let didCall = false;
670  process.on('unhandledRejection', () => {
671    assert(!didCall);
672    didCall = true;
673    const promise = Promise.reject(0);
674    process.nextTick(() => promise.catch(() => done()));
675  });
676});
677
678asyncTest(
679  'Promise rejection triggers unhandledRejection immediately',
680  function(done) {
681    clean();
682    Promise.reject(0);
683    process.on('unhandledRejection', common.mustCall((err) => {
684      if (timer) {
685        clearTimeout(timer);
686        timer = null;
687        done();
688      }
689    }));
690
691    let timer = setTimeout(common.mustNotCall(), 10000);
692  },
693);
694
695// https://github.com/nodejs/node/issues/30953
696asyncTest(
697  'Catching a promise should not take effect on previous promises',
698  function(done) {
699    onUnhandledSucceed(done, function(reason, promise) {
700      assert.strictEqual(reason, '1');
701    });
702    Promise.reject('1');
703    Promise.reject('2').catch(function() {});
704  }
705);
706