'use strict'; const debug = require("debug")("unhandled-rejection"); const EventEmitter = require("events").EventEmitter; const rejectionStore = require("./rejection-store"); let unhandledTimeout = 60 * 1000; let unhandledPromises = []; module.exports = function(options = {}) { let emitter = new EventEmitter(); let store = rejectionStore(options.timeout); function extractPromiseRejectionEvent(event) { let errorData = {}; if (event.detail != null && event.detail.reason != null) { errorData.error = event.detail.reason; } else if (event.reason != null) { errorData.error = event.reason; } if (event.detail != null && event.detail.promise != null) { errorData.promise = event.detail.promise; } else if (event.promise != null) { errorData.promise = event.promise; } return errorData; } function handleEvent(event, errorData) { if (event != null && event.preventDefault != null && typeof event.preventDefault === "function") { event.preventDefault(); } if (errorData == null) { if (event != null) { errorData = extractPromiseRejectionEvent(event); } } return errorData; } function deduplicateError(errorData) { /* This is to deal with the case where an unhandled rejection comes in through * more than one event interface, eg. in browser code. It returns a boolean * indicating whether to continue the emitting process. */ return (!store.exists(errorData)); } function handleUnhandledRejection(event, errorData) { debug("Got unhandledRejection"); let normalizedErrorData = handleEvent(event, errorData); if (deduplicateError(normalizedErrorData)) { store.register(normalizedErrorData); debug("Emitting unhandledRejection..."); emitter.emit("unhandledRejection", normalizedErrorData.error, normalizedErrorData.promise); } else { debug("Ignoring unhandledRejection as duplicate"); } } function handleRejectionHandled(event, errorData) { debug("Got rejectionHandled"); let normalizedErrorData = handleEvent(event, errorData); store.unregister(normalizedErrorData) debug("Emitting rejectionHandled..."); emitter.emit("rejectionHandled", normalizedErrorData.error, normalizedErrorData.promise); } function onunhandledrejectionHandler(reason, promise) { if (promise != null && promise.then != null) { /* First argument is an error. */ handleUnhandledRejection(null, {error: reason, promise: promise}); } else { /* First argument is an event. */ handleUnhandledRejection(reason); } } function onrejectionhandledHandler(promise) { if (promise.then != null) { /* First argument is a Promise. */ let errorData = store.find(promise); if (errorData != null) { handleRejectionHandled(null, errorData); } } else { /* First argument is an event. */ handleRejectionHandled(promise); } } function configureContextHandlers(context) { if (typeof context.onunhandledrejection === "function") { debug("Wrapping previous handler for .onunhandledrejection"); let _oldHandler = context.onunhandledrejection; context.onunhandledrejection = function(reason, promise) { onunhandledrejectionHandler(reason, promise); _oldHandler(event); } } else { context.onunhandledrejection = onunhandledrejectionHandler; } if (typeof context.onrejectionhandled === "function") { debug("Wrapping previous handler for .onrejectionhandled"); let _oldHandler = context.onrejectionhandled; context.onrejectionhandled = function(promise) { onrejectionhandledHandler(promise); _oldHandler(promise); } } else { context.onrejectionhandled = onrejectionhandledHandler; } } /* Bundlers like Webpack will shim `process`, but set its `.browser` property to `true`. */ let isWebWorker = (typeof WorkerGlobalScope !== "undefined"); let isNode = (typeof process !== "undefined" && process.browser !== true); let isBrowser = (!isNode && !isWebWorker && typeof document !== "undefined"); if (isNode) { debug("Detected environment: Node.js"); /* Bluebird, ES6 in Node.js */ process.on("unhandledRejection", (error, promise) => { let errorData = { error: error, promise: promise }; handleUnhandledRejection(null, errorData); }); process.on("rejectionHandled", (promise) => { let errorData = store.find(promise); if (errorData != null) { handleRejectionHandled(null, errorData); } }); } else if (isWebWorker) { debug("Detected environment: WebWorker"); /* Yaku, Bluebird, WHATWG Legacy(?) * The Bluebird documentation says self.addEventListener, but it seems to use on* handlers instead. */ configureContextHandlers(self); /* WHATWG */ self.addEventListener("unhandledrejection", handleUnhandledRejection); self.addEventListener("rejectionhandled", handleRejectionHandled); /* WhenJS (note the capitalization) - currently broken */ self.addEventListener("unhandledRejection", handleUnhandledRejection); self.addEventListener("rejectionHandled", handleRejectionHandled); } else if (isBrowser) { debug("Detected environment: Browser"); if (window.addEventListener != null) { debug("addEventListener is available, registering events..."); /* Bluebird, WHATWG */ window.addEventListener("unhandledrejection", handleUnhandledRejection); window.addEventListener("rejectionhandled", handleRejectionHandled); /* WhenJS (note the capitalization) - currently broken */ window.addEventListener("unhandledRejection", handleUnhandledRejection); window.addEventListener("rejectionHandled", handleRejectionHandled); } /* We will need to attempt to catch unhandled rejections using both the modern and * legacy APIs, because Yaku only supports the latter, *even* in modern browsers. */ debug("Configuring window.on* handlers..."); /* Bluebird (Legacy API), WHATWG (Legacy API), Yaku */ configureContextHandlers(window); } return emitter; }