1'use strict';
2
3const {
4  ArrayPrototypePush,
5  ArrayPrototypeShift,
6  Error,
7  ObjectDefineProperty,
8  ObjectPrototypeHasOwnProperty,
9  SafeWeakMap,
10} = primordials;
11
12const {
13  tickInfo,
14  promiseRejectEvents: {
15    kPromiseRejectWithNoHandler,
16    kPromiseHandlerAddedAfterReject,
17    kPromiseResolveAfterResolved,
18    kPromiseRejectAfterResolved,
19  },
20  setPromiseRejectCallback,
21} = internalBinding('task_queue');
22
23const { deprecate } = require('internal/util');
24
25const {
26  noSideEffectsToString,
27  triggerUncaughtException,
28} = internalBinding('errors');
29
30const {
31  pushAsyncContext,
32  popAsyncContext,
33  symbols: {
34    async_id_symbol: kAsyncIdSymbol,
35    trigger_async_id_symbol: kTriggerAsyncIdSymbol,
36  },
37} = require('internal/async_hooks');
38const { isErrorStackTraceLimitWritable } = require('internal/errors');
39
40// *Must* match Environment::TickInfo::Fields in src/env.h.
41const kHasRejectionToWarn = 1;
42
43const maybeUnhandledPromises = new SafeWeakMap();
44const pendingUnhandledRejections = [];
45const asyncHandledRejections = [];
46let lastPromiseId = 0;
47
48// --unhandled-rejections=none:
49// Emit 'unhandledRejection', but do not emit any warning.
50const kIgnoreUnhandledRejections = 0;
51
52// --unhandled-rejections=warn:
53// Emit 'unhandledRejection', then emit 'UnhandledPromiseRejectionWarning'.
54const kAlwaysWarnUnhandledRejections = 1;
55
56// --unhandled-rejections=strict:
57// Emit 'uncaughtException'. If it's not handled, print the error to stderr
58// and exit the process.
59// Otherwise, emit 'unhandledRejection'. If 'unhandledRejection' is not
60// handled, emit 'UnhandledPromiseRejectionWarning'.
61const kStrictUnhandledRejections = 2;
62
63// --unhandled-rejections=throw:
64// Emit 'unhandledRejection', if it's unhandled, emit
65// 'uncaughtException'. If it's not handled, print the error to stderr
66// and exit the process.
67const kThrowUnhandledRejections = 3;
68
69// --unhandled-rejections=warn-with-error-code:
70// Emit 'unhandledRejection', if it's unhandled, emit
71// 'UnhandledPromiseRejectionWarning', then set process exit code to 1.
72
73const kWarnWithErrorCodeUnhandledRejections = 4;
74
75let unhandledRejectionsMode;
76
77function setHasRejectionToWarn(value) {
78  tickInfo[kHasRejectionToWarn] = value ? 1 : 0;
79}
80
81function hasRejectionToWarn() {
82  return tickInfo[kHasRejectionToWarn] === 1;
83}
84
85function isErrorLike(o) {
86  return typeof o === 'object' &&
87         o !== null &&
88         ObjectPrototypeHasOwnProperty(o, 'stack');
89}
90
91function getUnhandledRejectionsMode() {
92  const { getOptionValue } = require('internal/options');
93  switch (getOptionValue('--unhandled-rejections')) {
94    case 'none':
95      return kIgnoreUnhandledRejections;
96    case 'warn':
97      return kAlwaysWarnUnhandledRejections;
98    case 'strict':
99      return kStrictUnhandledRejections;
100    case 'throw':
101      return kThrowUnhandledRejections;
102    case 'warn-with-error-code':
103      return kWarnWithErrorCodeUnhandledRejections;
104    default:
105      return kThrowUnhandledRejections;
106  }
107}
108
109function promiseRejectHandler(type, promise, reason) {
110  if (unhandledRejectionsMode === undefined) {
111    unhandledRejectionsMode = getUnhandledRejectionsMode();
112  }
113  switch (type) {
114    case kPromiseRejectWithNoHandler:
115      unhandledRejection(promise, reason);
116      break;
117    case kPromiseHandlerAddedAfterReject:
118      handledRejection(promise);
119      break;
120    case kPromiseResolveAfterResolved:
121      resolveError('resolve', promise, reason);
122      break;
123    case kPromiseRejectAfterResolved:
124      resolveError('reject', promise, reason);
125      break;
126  }
127}
128
129const multipleResolvesDeprecate = deprecate(
130  () => {},
131  'The multipleResolves event has been deprecated.',
132  'DEP0160',
133);
134function resolveError(type, promise, reason) {
135  // We have to wrap this in a next tick. Otherwise the error could be caught by
136  // the executed promise.
137  process.nextTick(() => {
138    if (process.emit('multipleResolves', type, promise, reason)) {
139      multipleResolvesDeprecate();
140    }
141  });
142}
143
144function unhandledRejection(promise, reason) {
145  const emit = (reason, promise, promiseInfo) => {
146    if (promiseInfo.domain) {
147      return promiseInfo.domain.emit('error', reason);
148    }
149    return process.emit('unhandledRejection', reason, promise);
150  };
151
152  maybeUnhandledPromises.set(promise, {
153    reason,
154    uid: ++lastPromiseId,
155    warned: false,
156    domain: process.domain,
157    emit,
158  });
159  // This causes the promise to be referenced at least for one tick.
160  ArrayPrototypePush(pendingUnhandledRejections, promise);
161  setHasRejectionToWarn(true);
162}
163
164function handledRejection(promise) {
165  const promiseInfo = maybeUnhandledPromises.get(promise);
166  if (promiseInfo !== undefined) {
167    maybeUnhandledPromises.delete(promise);
168    if (promiseInfo.warned) {
169      const { uid } = promiseInfo;
170      // Generate the warning object early to get a good stack trace.
171      // eslint-disable-next-line no-restricted-syntax
172      const warning = new Error('Promise rejection was handled ' +
173                                `asynchronously (rejection id: ${uid})`);
174      warning.name = 'PromiseRejectionHandledWarning';
175      warning.id = uid;
176      ArrayPrototypePush(asyncHandledRejections, { promise, warning });
177      setHasRejectionToWarn(true);
178      return;
179    }
180  }
181  if (maybeUnhandledPromises.size === 0 && asyncHandledRejections.length === 0)
182    setHasRejectionToWarn(false);
183}
184
185const unhandledRejectionErrName = 'UnhandledPromiseRejectionWarning';
186function emitUnhandledRejectionWarning(uid, reason) {
187  const warning = getErrorWithoutStack(
188    unhandledRejectionErrName,
189    'Unhandled promise rejection. This error originated either by ' +
190      'throwing inside of an async function without a catch block, ' +
191      'or by rejecting a promise which was not handled with .catch(). ' +
192      'To terminate the node process on unhandled promise ' +
193      'rejection, use the CLI flag `--unhandled-rejections=strict` (see ' +
194      'https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). ' +
195      `(rejection id: ${uid})`,
196  );
197  try {
198    if (isErrorLike(reason)) {
199      warning.stack = reason.stack;
200      process.emitWarning(reason.stack, unhandledRejectionErrName);
201    } else {
202      process.emitWarning(
203        noSideEffectsToString(reason), unhandledRejectionErrName);
204    }
205  } catch {
206    try {
207      process.emitWarning(
208        noSideEffectsToString(reason), unhandledRejectionErrName);
209    } catch {
210      // Ignore.
211    }
212  }
213
214  process.emitWarning(warning);
215}
216
217// If this method returns true, we've executed user code or triggered
218// a warning to be emitted which requires the microtask and next tick
219// queues to be drained again.
220function processPromiseRejections() {
221  let maybeScheduledTicksOrMicrotasks = asyncHandledRejections.length > 0;
222
223  while (asyncHandledRejections.length > 0) {
224    const { promise, warning } = ArrayPrototypeShift(asyncHandledRejections);
225    if (!process.emit('rejectionHandled', promise)) {
226      process.emitWarning(warning);
227    }
228  }
229
230  let len = pendingUnhandledRejections.length;
231  while (len--) {
232    const promise = ArrayPrototypeShift(pendingUnhandledRejections);
233    const promiseInfo = maybeUnhandledPromises.get(promise);
234    if (promiseInfo === undefined) {
235      continue;
236    }
237    promiseInfo.warned = true;
238    const { reason, uid, emit } = promiseInfo;
239
240    let needPop = true;
241    const {
242      [kAsyncIdSymbol]: promiseAsyncId,
243      [kTriggerAsyncIdSymbol]: promiseTriggerAsyncId,
244    } = promise;
245    // We need to check if async_hooks are enabled
246    // don't use enabledHooksExist as a Promise could
247    // come from a vm.* context and not have an async id
248    if (typeof promiseAsyncId !== 'undefined') {
249      pushAsyncContext(
250        promiseAsyncId,
251        promiseTriggerAsyncId,
252        promise,
253      );
254    }
255    try {
256      switch (unhandledRejectionsMode) {
257        case kStrictUnhandledRejections: {
258          const err = isErrorLike(reason) ?
259            reason : generateUnhandledRejectionError(reason);
260          // This destroys the async stack, don't clear it after
261          triggerUncaughtException(err, true /* fromPromise */);
262          if (typeof promiseAsyncId !== 'undefined') {
263            pushAsyncContext(
264              promise[kAsyncIdSymbol],
265              promise[kTriggerAsyncIdSymbol],
266              promise,
267            );
268          }
269          const handled = emit(reason, promise, promiseInfo);
270          if (!handled) emitUnhandledRejectionWarning(uid, reason);
271          break;
272        }
273        case kIgnoreUnhandledRejections: {
274          emit(reason, promise, promiseInfo);
275          break;
276        }
277        case kAlwaysWarnUnhandledRejections: {
278          emit(reason, promise, promiseInfo);
279          emitUnhandledRejectionWarning(uid, reason);
280          break;
281        }
282        case kThrowUnhandledRejections: {
283          const handled = emit(reason, promise, promiseInfo);
284          if (!handled) {
285            const err = isErrorLike(reason) ?
286              reason : generateUnhandledRejectionError(reason);
287              // This destroys the async stack, don't clear it after
288            triggerUncaughtException(err, true /* fromPromise */);
289            needPop = false;
290          }
291          break;
292        }
293        case kWarnWithErrorCodeUnhandledRejections: {
294          const handled = emit(reason, promise, promiseInfo);
295          if (!handled) {
296            emitUnhandledRejectionWarning(uid, reason);
297            process.exitCode = 1;
298          }
299          break;
300        }
301      }
302    } finally {
303      if (needPop) {
304        if (typeof promiseAsyncId !== 'undefined') {
305          popAsyncContext(promiseAsyncId);
306        }
307      }
308    }
309    maybeScheduledTicksOrMicrotasks = true;
310  }
311  return maybeScheduledTicksOrMicrotasks ||
312         pendingUnhandledRejections.length !== 0;
313}
314
315function getErrorWithoutStack(name, message) {
316  // Reset the stack to prevent any overhead.
317  const tmp = Error.stackTraceLimit;
318  if (isErrorStackTraceLimitWritable()) Error.stackTraceLimit = 0;
319  // eslint-disable-next-line no-restricted-syntax
320  const err = new Error(message);
321  if (isErrorStackTraceLimitWritable()) Error.stackTraceLimit = tmp;
322  ObjectDefineProperty(err, 'name', {
323    __proto__: null,
324    value: name,
325    enumerable: false,
326    writable: true,
327    configurable: true,
328  });
329  return err;
330}
331
332function generateUnhandledRejectionError(reason) {
333  const message =
334    'This error originated either by ' +
335    'throwing inside of an async function without a catch block, ' +
336    'or by rejecting a promise which was not handled with .catch().' +
337    ' The promise rejected with the reason ' +
338    `"${noSideEffectsToString(reason)}".`;
339
340  const err = getErrorWithoutStack('UnhandledPromiseRejection', message);
341  err.code = 'ERR_UNHANDLED_REJECTION';
342  return err;
343}
344
345function listenForRejections() {
346  setPromiseRejectCallback(promiseRejectHandler);
347}
348module.exports = {
349  hasRejectionToWarn,
350  setHasRejectionToWarn,
351  listenForRejections,
352  processPromiseRejections,
353};
354