Initial commit

master
Sven Slootweg 8 years ago
parent 9f52a92760
commit 13d186bdcd

@ -0,0 +1,3 @@
{
"presets": ["es2015"]
}

@ -0,0 +1 @@
/node_modules/

@ -0,0 +1,3 @@
## 1.0.0 (August 19, 2016)
Initial release.

@ -0,0 +1,107 @@
# unhandled-rejection
Unfortunately, there's quite a bit of variation between different Promises implementations and environments, in how you are supposed to (globally) detect unhandled rejections. This can make it tricky to reliably log such errors, *especially* in browsers.
This module provides a single unified API for detecting unhandled rejections (and their subsequent handling, if any). It works in Browserified/Webpacked environments as well, as long as the Promises implementations support it.
__Don't ignore unhandled errors.__ This module is meant to be used for implementing *log-and-crash* logic - once an unexpected error (ie. bug) occurs, it is *not safe to continue*, as your application is now in an unpredictable state. You should instead let your application crash after logging the error, to start over with a 'clean slate'.
## Supported implementations
If your favourite Promises implementation or environment is not (correctly) supported by this module yet, please open a ticket!
| | Node.js | WebWorkers | Modern browsers | Old browsers |
|-------------------------|---------|------------|-----------------|--------------|
| Bluebird (> 2.7.0) | ✓ | ✓ | ✓ | ✓ |
| ES6 Promises (Node.js)¹ | ✓ | n/a | n/a | n/a |
| ES6 Promises (WHATWG)² | n/a | ✓ | ✓ | n/a |
| Q | ✓ | ✗ | ✗ | ✗ |
| WhenJS | ✓ | ✓⁴ | ✓⁴ | ✗ |
| Yaku | ✓ | ✓ | ✓ | ✓ |
| es6-promise | ✗ | ✗ | ✗ | ✗ |
| then/promise | ✗ | ✗ | ✗ | ✗ |
| (other, per spec)³ | ✓ | n/a | n/a | n/a |
| Symbol | Meaning |
|--------|----------------------------------------------|
| ✓ | Implemented and supported. |
| ✗ | Not implemented - library does not support global rejection events in this environment. |
| n/a | Specification does not cover this environment. |
| ¹ | [Node.js implementation](https://nodejs.org/api/process.html#process_event_rejectionhandled) |
| ² | [WHATWG specification](https://html.spec.whatwg.org/multipage/webappapis.html#unhandled-promise-rejections) |
| ³ | [@benjamingr's specification](https://gist.github.com/benjamingr/0237932cee84712951a2) |
| ⁴ | Implementation exists, but is currently completely broken in bundled environments, such as Webpack and Browserify. ([issue](https://github.com/cujojs/when/issues/490)) |
## License
[WTFPL](http://www.wtfpl.net/txt/copying/) or [CC0](https://creativecommons.org/publicdomain/zero/1.0/), whichever you prefer. A donation and/or attribution are appreciated, but not required.
## Donate
Maintaining open-source projects takes a lot of time, and the more donations I receive, the more time I can dedicate to open-source. If this module is useful to you, consider [making a donation](http://cryto.net/~joepie91/donate.html)!
You can donate using Bitcoin, PayPal, Flattr, cash-in-mail, SEPA transfers, and pretty much anything else. Thank you!
## Contributing
Pull requests welcome. Please make sure your modifications are in line with the overall code style, and ensure that you're editing the files in `src/`, not those in `lib/`.
Build tool of choice is `gulp`; simply run `gulp` while developing, and it will watch for changes.
Be aware that by making a pull request, you agree to release your modifications under the licenses stated above.
__Make sure to read the `implementation-details.md` beforehand.__ The various `unhandledRejection` APIs are extremely finicky, and it's tricky to work out exactly how you can make everything play together nicely. When making a pull request, make sure to *intensively check* that you haven't broken anything in the process.
## Usage
```javascript
const unhandledRejection = require("unhandled-rejection");
// Assuming `loggingServer` is some kind of logging API...
let rejectionEmitter = unhandledRejection({
timeout: 20
});
rejectionEmitter.on("unhandledRejection", (error, promise) => {
loggingServer.registerError(error);
});
rejectionEmitter.on("rejectionHandled", (error, promise) => {
loggingServer.registerHandled(error);
})
```
## Rejections and timeouts
Due to the asynchronous nature of Promises, it's never *completely* certain that a rejection is unhandled - for example, a handler may be attached at a later point. For this reason, many Promise implementations provide a `rejectionHandled` event that essentially 'rectifies' an earlier `unhandledRejection` event, indicating that it was handled after all.
Because not all of these APIs are consistent, this module has to internally keep track of unhandled promises that it has encountered. The problem is that this means it has to keep a reference to every `Error` that it encounters, which in turn will get in the way of the garbage collector - eventually creating a memory leak.
To prevent this, you can configure a `timeout` setting (defaulting to `60` seconds), after which the module will consider a rejection to have been 'final', so that it can remove it from its internal list, thereby freeing up memory. If a `rejectionHandled` for the error comes in *after* this timeout, it will be silently ignored.
Situations where it takes more than 60 seconds to attach a `.catch` handler are rare, but *if* you run into such a situation, you can increase the `timeout` value to accommodate that.
## API
### unhandledRejection(options)
Returns a new `emitter`.
* __options__:
* __timeout:__ *Defaults to `60`.* The amount of time after which an unhandled rejection should be considered 'final'. See the "Timeout" section above for more information.
### emitter.on("unhandledRejection", callback)
Emitted whenever an unhandled rejection is encountered. The `callback` arguments are as follows:
* __error:__ The Error object (or other error value) that the rejection occurred with.
* __promise:__ The Promise for which the error occurred.
### emitter.on("rejectionHandled", callback)
Emitted when a previously 'unhandled' rejection was handled after all (within the timeout). The `callback` arguments are as follows:
* __error:__ The Error object (or other error value) that the original rejection occurred with.
* __promise:__ The Promise for which the error originally occurred.

@ -0,0 +1,18 @@
var gulp = require("gulp");
var presetES2015 = require("@joepie91/gulp-preset-es2015");
var source = ["src/**/*.js"]
gulp.task('babel', function() {
return gulp.src(source)
.pipe(presetES2015({
basePath: __dirname
}))
.pipe(gulp.dest("lib/"));
});
gulp.task("watch", function () {
gulp.watch(source, ["babel"]);
});
gulp.task("default", ["babel", "watch"]);

@ -0,0 +1,65 @@
## Trivia
* Some APIs use `unhandledRejection`, some use `unhandledrejection`. Note the capitalization difference.
* WhenJS is basically [completely broken](https://github.com/cujojs/when/issues/490) in bundled environments. It doesn't seem like this will be fixed any time soon...
* Bluebird claims that you should use `self.addEventListener` in a WebWorker context, but that does not actually appear to work - instead, I've used `self.onunhandledrejection`, which definitely *does* work.
* Due to the subtle differences between implementations, and the fact that different libraries respond differently to having both the modern and legacy event handler defined, you actually need to *deduplicate* errors to handle all rejections correctly.
* Some events come in a wrapper object. Some do not.
* What a mess...
## Implementation chart (according to the documentation and specs, at least...)
Bluebird (http://bluebirdjs.com/docs/api/error-management-configuration.html#global-rejection-events)
* `process.on//unhandledRejection`: __(Node.js)__ Potentially unhandled rejection.
* `process.on//rejectionHandled`: __(Node.js)__ Cancel unhandled rejection, it was handled anyway.
* `self.addEventListener//unhandledrejection`: __(WebWorkers)__ Potentially unhandled rejection.
* `self.addEventListener//rejectionhandled`: __(WebWorkers)__ Cancel unhandled rejection, it was handled anyway.
* `window.addEventListener//unhandledrejection`: __(Modern browsers, IE >= 9)__ Potentially unhandled rejection.
* `window.addEventListener//rejectionhandled`: __(Modern browsers, IE >= 9)__ Cancel unhandled rejection, it was handled anyway.
* `window.onunhandledrejection`: __(IE >= 6)__ Potentially unhandled rejection.
* `window.onrejectionhandled`: __(IE >= 6)__ Cancel unhandled rejection, it was handled anyway.
WhenJS (https://github.com/cujojs/when/blob/3.7.0/docs/debug-api.md)
* `process.on//unhandledRejection`: __(Node.js)__ Potentially unhandled rejection.
* `process.on//rejectionHandled`: __(Node.js)__ Cancel unhandled rejection, it was handled anyway.
* `self.addEventListener//unhandledrejection`: __(WebWorkers, Modern browsers, IE >= 9)__ Potentially unhandled rejection.
* `self.addEventListener//rejectionhandled`: __(WebWorkers, Modern browsers, IE >= 9)__ Cancel unhandled rejection, it was handled anyway.
Spec (https://gist.github.com/benjamingr/0237932cee84712951a2)
* `process.on//unhandledRejection`: __(Node.js)__ Potentially unhandled rejection.
* `process.on//rejectionHandled`: __(Node.js)__ Cancel unhandled rejection, it was handled anyway.
Q (https://github.com/kriskowal/q/blob/e01d7b2875e784954e11a1668551288f5ffe46cc/q.js#L1037-L1113)
* `process.on//unhandledRejection`: __(Node.js)__ Potentially unhandled rejection.
* `process.on//rejectionHandled`: __(Node.js)__ Cancel unhandled rejection, it was handled anyway.
Spec (WHATWG: https://html.spec.whatwg.org/multipage/webappapis.html#unhandled-promise-rejections)
* `<browsingContext>.addEventListener//unhandledrejection`: __(Browsers, WebWorkers)__ Potentially unhandled rejection.
* `<browsingContext>.addEventListener//rejectionhandled`: __(Browsers, WebWorkers)__ Cancel unhandled rejection, it was handled anyway.
* `window.onunhandledrejection`: __(Browsers)__ Potentially unhandled rejection.
* `window.onrejectionhandled`: __(Browsers)__ Cancel unhandled rejection, it was handled anyway.
ES6 Promises in Node.js (https://nodejs.org/api/process.html#process_event_rejectionhandled onwards)
* `process.on//unhandledRejection`: Potentially unhandled rejection.
* `process.on//rejectionHandled`: Cancel unhandled rejection, it was handled anyway.
Yaku (https://github.com/ysmood/yaku#unhandled-rejection)
* `process.on//unhandledRejection`: __(Node.js)__ Potentially unhandled rejection.
* `process.on//rejectionHandled`: __(Node.js)__ Cancel unhandled rejection, it was handled anyway.
* `window.onunhandledrejection`: __(Browsers)__ Potentially unhandled rejection.
* `window.onrejectionhandled`: __(Browsers)__ Cancel unhandled rejection, it was handled anyway.
`then/promise`
* Currently not implemented. ([issue](https://github.com/then/promise/issues/70))
`es6-promise`
* Currently not implemented. ([issue](https://github.com/stefanpenner/es6-promise/issues/70))

@ -0,0 +1,3 @@
'use strict';
module.exports = require("./lib");

@ -0,0 +1,39 @@
{
"name": "unhandled-rejection",
"version": "1.0.0",
"description": "Catch unhandled rejections, no matter what Promises implementation they come from",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build-test": "webpack --config webpack.config.browser.js && webpack --config webpack.config.webworker.js"
},
"repository": {
"type": "git",
"url": "http://git.cryto.net/joepie91/node-unhandled-rejection.git"
},
"keywords": [
"promises",
"bluebird",
"errors",
"error handling",
"debugging"
],
"author": "Sven Slootweg",
"license": "WTFPL",
"dependencies": {
"debug": "^2.2.0"
},
"devDependencies": {
"@joepie91/gulp-preset-es2015": "^1.0.1",
"babel-loader": "^6.2.4",
"babel-preset-es2015": "^6.6.0",
"bluebird": "^3.4.1",
"es6-promise": "^3.2.1",
"gulp": "^3.9.1",
"q": "^1.4.1",
"strip-loader": "^0.1.2",
"webpack": "^1.13.1",
"when": "^3.7.7",
"yaku": "^0.15.9"
}
}

@ -0,0 +1,192 @@
'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 <context>.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 <context>.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;
}

@ -0,0 +1,28 @@
'use strict';
module.exports = function(timeout = 60) {
let rejections = [];
return {
register: function(errorData) {
rejections.push(errorData);
setTimeout(() => {
this.unregister(errorData);
}, timeout * 1000);
},
unregister: function(errorData) {
let errorIndex = rejections.indexOf(errorData);
if (errorIndex !== -1) {
rejections.splice(errorIndex, 1);
}
},
exists: function(errorData) {
return rejections.some((item) => item.promise === errorData.promise && item.error === errorData.error);
},
find: function(promise) {
return rejections.find((item) => item.promise === promise);
}
}
}

File diff suppressed because one or more lines are too long

@ -0,0 +1,10 @@
<html>
<head>
<meta charset="UTF-8">
<title>unhandled-rejection</title>
<script src="test.bundle.js"></script>
</head>
<body>
</body>
</html>

@ -0,0 +1,80 @@
'use strict';
let isWebWorker = (typeof WorkerGlobalScope !== "undefined");
let isNode = (typeof process !== "undefined" && process.browser !== true);
let isBrowser = (!isNode && !isWebWorker && typeof document !== "undefined");
let environmentName = "";
if (isWebWorker) {
environmentName = "WebWorker";
} else if (isBrowser) {
environmentName = "Browser";
} else if (isNode) {
environmentName = "Node";
} else {
environmentName = "Unknown environment";
}
function supportsWebWorkers() {
return !!window.Worker;
}
if (isBrowser || isWebWorker) {
const debug = require("debug");
debug.enable("*");
}
const unhandledRejection = require("./");
let emitter = unhandledRejection();
emitter.on("unhandledRejection", (error, promise) => {
console.log(`Caught an error in <${environmentName}>! ${error.message}`)
});
const BPromise = require("bluebird");
const QPromise = require("q");
const WPromise = require("when");
const YPromise = require("yaku");
/* Workaround for stefanpenner/es6-promise#183 */
let _nativePromise = Promise;
const EPromise = require("es6-promise").Promise;
Promise = _nativePromise;
Promise.resolve().then(() => {
console.log("Throwing Native...")
throw new Error("Native")
});
BPromise.resolve().then(() => {
console.log("Throwing Bluebird...")
throw new Error("Bluebird")
});
QPromise.resolve().then(() => {
console.log("Throwing Q...")
throw new Error("Q")
});
WPromise.resolve().then(() => {
console.log("Throwing WhenJS...")
throw new Error("WhenJS")
});
YPromise.resolve().then(() => {
console.log("Throwing Yaku...")
throw new Error("Yaku")
});
/* Disabled until stefanpenner/es6-promise#70 is resolved
EPromise.resolve().then(() => {
console.log("Throwing es6-promise...")
throw new Error("es6-promise")
});
*/
if (isBrowser && supportsWebWorkers) {
let worker = new Worker("test.webworker.js");
}

File diff suppressed because one or more lines are too long

@ -0,0 +1,13 @@
module.exports = {
entry: "./test/test.js",
output: {
path: __dirname,
filename: "./test/test.bundle.js"
},
cache: false,
module: {
loaders: [
{ test: /\.js/, loader: "babel" }
]
}
};

@ -0,0 +1,13 @@
module.exports = {
entry: "./test/test.js",
output: {
path: __dirname,
filename: "./test/test.webworker.js"
},
cache: false,
module: {
loaders: [
{ test: /\.js/, loader: "strip-loader?strip[]=debug!babel" }
]
}
};
Loading…
Cancel
Save