Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for "Unhandled promise rejection" / "Uncaught (in promise)" #424

Closed
benvinegar opened this issue Nov 18, 2015 · 41 comments
Closed

Comments

@benvinegar
Copy link
Contributor

Example use case (JSBin):

var p = new Promise(function(resolve, reject) {
    window.setTimeout(
        function() {
            reject();
        }, 10);
});

p.then(function () {
    console.log('done!');
});

If the promise throws an exception, and there's no error handler, the browser will spit out an uncaught promise error.

image

@benvinegar
Copy link
Contributor Author

Now, an argument could be made that an unhandled promise rejection shouldn't be caught by Raven ... because sometimes you intentionally choose not to handle rejection.

An example in non-Promise world would be to create an XMLHttpRequest without a traditional error handler (e.g. no error property in $.ajax). If you forget the error handler this way, there will be no console exception. But, if you use promises with $.ajax and omit catch, the same failure will throw an error.

@joaocunha
Copy link

In our case, it happens in a React Component context:

import React, {Component} from 'react';

yoMammaStinks(a[0]);

export default class InlineText extends Component {
  render() {
    return (
      <p className="foo">
        <b>bar</b>
      </p>
    );
  }
}

image


Even tho the error is raised, it's not tracked by Raven.

@benvinegar
Copy link
Contributor Author

Why you talkin' that way 'bout my momma?

But that's a great example, and in this case, absolutely it should be caught.

Now ... do we add this to the "react" plugin? Or do we add a "promises" plugin, and then instruct React users to use this? Or just make it on by default? cc @mattrobenolt

@benvinegar
Copy link
Contributor Author

@joaocunha – so @mattrobenolt and I have had a discussion about this. It seems like React your module loader has wrapped loading the module in a try/catch, and instead of re-throwing the ReferenceError, it passes it down via the Promise rejection. Because the error isn't actually thrown, our onerror handler doesn't catch it and we instead get this Unhandled promise.

So ... we feel this might be a React module-loader-specific thing. We're going to dig further into React, make sure we have a good handle on this, then get back to you.

If anyone has similar examples re: uncaught promise rejections, we'd love to hear 'em.

@joaocunha
Copy link

Great to know you guys found the starting point. Really appreciated.

In the meanwhile, should we be using the React build of Raven? How do we do that with npm and CommonJS? We are currently loading react-js with ES6's import Raven from 'react-js'.

@joaocunha
Copy link

@benvinegar also, let me know if you need more info about our stack. We're using an express server, Webpack and Redux. Adding to that, we're not doing isomorphic/universal JS at the moment. These are the dependencies we currently have in:

"dependencies": {
    "assets-webpack-plugin": "^3.1.0",
    "autoprefixer": "^6.0.3",
    "autoprefixer-loader": "^3.1.0",
    "babel": "^5.8.29",
    "babel-core": "^5.8.29",
    "babel-eslint": "^4.1.3",
    "babel-loader": "^5.3.2",
    "babel-plugin-react-transform": "^1.1.1",
    "babel-plugin-typecheck": "^1.3.0",
    "babel-runtime": "^5.8.29",
    "basic-auth": "^1.0.3",
    "better-npm-run": "0.0.3",
    "body-parser": "^1.14.1",
    "bourbon": "^4.2.6",
    "classnames": "^2.2.0",
    "clean-webpack-plugin": "^0.1.3",
    "component-cookie": "^1.1.2",
    "compression": "^1.6.0",
    "concurrently": "^0.1.1",
    "cookie-parser": "^1.4.0",
    "css-loader": "^0.21.0",
    "declarative-promise": "^0.1.12",
    "es6-promise": "^3.0.2",
    "exports-loader": "^0.6.2",
    "express": "^4.13.3",
    "express-webpack-assets": "0.0.2",
    "extract-text-webpack-plugin": "^0.8.2",
    "file-loader": "^0.8.4",
    "history": "^1.12.6",
    "humps": "^1.0.0",
    "imports-loader": "^0.6.5",
    "intl": "^1.0.1",
    "ip": "^1.0.1",
    "jade": "^1.11.0",
    "json-loader": "^0.5.3",
    "jsonwebtoken": "^5.4.1",
    "lodash": "^3.10.1",
    "lru-memoize": "^1.0.0",
    "map-props": "^1.0.0",
    "morgan": "^1.6.1",
    "multireducer": "^1.0.2",
    "node-sass": "^3.4.1",
    "normalize.css": "^3.0.3",
    "normalizr": "^1.4.0",
    "oauth": "^0.9.14",
    "parse-link-header": "^0.4.1",
    "piping": "^0.3.0",
    "postcss-loader": "^0.7.0",
    "precss": "^1.3.0",
    "pretty-error": "^1.2.0",
    "qs": "^5.2.0",
    "query-string": "^3.0.0",
    "raven": "^0.8.1",
    "raven-js": "^1.3.0",
    "react": "^0.14.0",
    "react-a11y": "^0.2.6",
    "react-addons-update": "^0.14.2",
    "react-dom": "^0.14.0",
    "react-helmet": "^2.1.1",
    "react-inline-css": "^2.0.0",
    "react-intl": "^2.0.0-beta-1",
    "react-list": "^0.7.3",
    "react-redux": "^4.0.0",
    "react-router": "^1.0.0-rc3",
    "react-transform-hmr": "^1.0.1",
    "redux": "^3.0.4",
    "redux-api-middleware": "^1.0.0-beta3",
    "redux-logger": "^2.0.4",
    "redux-router": "^1.0.0-beta3",
    "redux-thunk": "^1.0.0",
    "sass-loader": "^3.1.1",
    "serve-favicon": "^2.3.0",
    "serve-static": "^1.10.0",
    "strip-loader": "^0.1.0",
    "style-loader": "^0.13.0",
    "svg-inline-loader": "^0.3.0",
    "truncate": "^2.0.0",
    "url-loader": "^0.5.6",
    "webpack": "^1.12.2",
    "webpack-dev-middleware": "^1.2.0",
    "webpack-hot-middleware": "^2.4.1",
    "whatwg-fetch": "^0.10.1"
  },
  "devDependencies": {
    "browser-sync": "^2.9.11",
    "browser-sync-webpack-plugin": "^1.0.0",
    "eslint": "^1.7.3",
    "eslint-config-airbnb": "^0.1.0",
    "eslint-loader": "^1.1.0",
    "eslint-plugin-import": "^0.8.1",
    "eslint-plugin-react": "^3.6.3",
    "react-transform-catch-errors": "^1.0.0",
    "react-transform-hmr": "^1.0.1",
    "redbox-react": "^1.1.1",
    "redux-devtools": "^3.0.0-beta-3",
    "redux-devtools-dock-monitor": "^1.0.0-beta-3",
    "redux-devtools-log-monitor": "^1.0.0-beta-3",
    "webpack-dev-middleware": "^1.2.0",
    "webpack-hot-middleware": "^2.4.1"
  },

@joaocunha
Copy link

@benvinegar We changed the API and, as it was not matching the new specs, it raised some errors. Straight from core-js: https://github.com/zloirock/core-js/blob/master/modules/es6.promise.js#L127

image

It seems it won't ever capture exceptions in that context.

@benvinegar
Copy link
Contributor Author

@joaocunha – looking at the code above, it looks like you could do the following as a workaround:

window.onunhandledrejection = function(data) {
    Raven.captureException(data.reason);
}

@benvinegar
Copy link
Contributor Author

Oops wrong button :)

@benvinegar benvinegar reopened this Nov 18, 2015
@joaocunha
Copy link

That workaround does work indeed, thanks!

@mattrobenolt
Copy link
Contributor

Thanks for verifying @joaocunha. :)

I'm not sure if we'll want to add this into raven-js core though since this window.onunhandledrejection is not a standard at all.

I'm going to just drop some links about this topic here for reference:

https://groups.google.com/a/chromium.org/forum/#!topic/blink-dev/SRsFwwEgnUQ
https://github.com/domenic/unhandled-rejections-browser-spec
https://developer.mozilla.org/en-US/docs/Web/API/Window/onunhandledrejection
https://codereview.chromium.org/1179113007

I'd want to research and see if this is actually implemented by the popular Promise polyfills and if it's starting to get implemented in browsers (looks like Blink merged in support), I'll happily add some native support like we do for window.onerror. I just want to make sure the API for the callback is stable and doesn't change around.

@mattrobenolt
Copy link
Contributor

Also, since it passes the promise itself through as data.promise, curious what we could extract from that and pass along as extra data.

@benvinegar
Copy link
Contributor Author

Note that we've since documented how to work with Promises in the docs: https://docs.getsentry.com/hosted/clients/javascript/usage/#promises

@jabooth
Copy link

jabooth commented Jan 14, 2016

It seems steps are been taken to standardise the window.onunhandledrejection callback:
tc39/ecma262#76

@benvinegar
Copy link
Contributor Author

@jabooth – thanks for linking this.

I'm still not sure that this should be done by default, though. I think the code example we've added to the docs is fine for now.

@joaomilho
Copy link

I've tried it and all it presents from the object in Sentry's interface is "[object PromiseRejectionEvent]".

Using the same code as in the docs:

window.addEventListener('unhandledrejection', (err)  => {
  raven.captureException(err);
});

err looks like this (latest chrome mac):
screen shot 2016-01-21 at 12 05 25 pm

@benvinegar
Copy link
Contributor Author

@joaomilho – Oops. I'm looking for a specification for PromiseRejectionEvent, but I can't seem to find out.

But from your example above, and from the relevant Bluebird tests, it looks like the Error object is the reason property. In which case, I'd do:

window.addEventListener('unhandledrejection', (err)  => {
  Raven.captureException(err.reason);
});

I'm going to update the docs.

@benvinegar
Copy link
Contributor Author

@joaomilho – So, I played around with this, and the thing is, reason is non-standard and could contain anything. It could be a string, it could be an error, etc.

The examples from MDN actually demonstrate passing both an error and a string: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/reject

The problem here is that Raven.captureException purely captures Error objects, and Raven.captureMessage captures strings.

Perhaps the best solution is:

window.addEventListener('unhandledrejection', (err)  => {
  var reason = err.reason;
  if (err instanceof Error) {
    Raven.captureException(reason);
  } else if (Object.prototype.toString.call(reason) === '[object String]') {
    Raven.captureMessage(reason);
  }
});

We could also introduce a function, say, Raven.capture that tries to call the appropriate function based on the type of the passed arguments.

Edit: @mattrobenolt points out that Raven.captureException automatically falls back to messages, so this if statement is not necessary. Just use captureException.

@mattrobenolt
Copy link
Contributor

@benvinegar fwiw: https://github.com/getsentry/raven-js/blob/master/src/raven.js#L292-L293

captureException falls back to just calling captureMessage already if it's not an Error object.

@benvinegar
Copy link
Contributor Author

Bam, perfect.

@mattrobenolt
Copy link
Contributor

@joaomilho
Copy link

So, just to be 100% clear,

window.addEventListener('unhandledrejection', (err)  => {
    Raven.captureException(err.reason);
});

would be enough, right?

Another question, since I'm using fetch, all errors will go to my catch handler. But if the error wasn't meant to be, it will not pass to it a response.

Example:

// checkResponse will return the response or throw it based on "ok"
fetch(myFetchConfig).then(checkResponse)
      .then((response) => {
        ///... some correct code
        someFnThatDoesntExist()
      })
      .catch((response) => {
        // try to do something with response, like response.json().then(...)
      })

In this case, I'll get an err object instead of a response object in my catch. So I'll get an error in sentry related to trying to use the response object, like "TypeError: response.json is not a function" instead of "Uncaught ReferenceError: someFnThatDoesntExist is not defined".

What do you this is the best approach to handle it?

@joaomilho
Copy link

Doing a simple check solves it, like:

        if(!(response instanceof Response ))
          throw response

But it seems incorrect to me to do it on every catch handler.

@danharper
Copy link

I'm using Babel (which uses core-js), to get it to work there I've had to:

  1. delete window.Promise at the very start of the app, before loading any other code to ensure core-js's Promise polyfill is used instead
  2. register a handler as window.onunhandledrejection = .. instead of window.addEventListener('unhandledrejection', ..)

My handler looks like:

window.onunhandledrejection = e => {
  console.warn('Unhandled Promise Rejection', e.reason)
  Raven.captureException(e.reason, {
    extra: { unhandledPromise: true }
  })
}

And to delete the native Promise, I've got a file src/disableNativePromises.js

if (window.Promise) {
  console.debug(`This browser provides a native Promise. I'm disabling it to force use of core-js to allow "onunhandledrejection".`)
  delete window.Promise
}

and I'm loading that as the first file in my entry config for Webpack:

module.exports  = {
  entry: [
    './src/disableNativePromises',
    'babel-polyfill',
    './src/index'
  ],
  // ...
}

@benvinegar
Copy link
Contributor Author

@joaomilho – any chance you could throw up a JSBin of a fully-functioning example?

@benvinegar
Copy link
Contributor Author

I'm using Babel (which uses core-js), to get it to work there I've had to ...

Painful. If I'm reading this correctly, I guess this is because Babel sees the native Promise, and doesn't bother to polyfill. Except, native Promise doesn't support onunhandledrejection in (any?) browsers.

Hmm, I wonder how we should document this ... or perhaps open a ticket on Babel?

@joaocunha
Copy link

register a handler as window.onunhandledrejection = .. instead of window.addEventListener('unhandledrejection', ..)

^ Same here.

@danharper
Copy link

It's a core-js issue, not Babel (Babel just uses core-js for most polyfills afaik).

But yeah, the issue is core-js currently doesn't take the Promise rejection events into account when deciding whether it should replace the native Promise implementation.

There's a discussion thread here: zloirock/core-js#140, and there's a warning in their readme here.

@joaomilho
Copy link

@benvinegar added to my backlog, but can't promise a deadline ;)

@ghost
Copy link

ghost commented Feb 22, 2016

zloirock/core-js#140 was resolved, though I wouldn't close this issue until a suitable workaround can be automated. awesome project!

@ghost
Copy link

ghost commented Feb 22, 2016

Here's an example Gist implementing @benvinegar original workaround, with backoff for latent network connections (handy if you're loading this library via the Sentry integration for Segment).

https://gist.github.com/jhabdas/102bd7e7b856726b3c6e

@jessepollak
Copy link

I'm running into this issue and I've added the window.onunhandledrejection and it's not doing anything. Oddly enough, I see the rejection on raven.js:278 in the wrapped function.

screenshot 2016-08-24 17 42 42

Any ideas?

@MarkMurphy
Copy link

what's the TL;DR solution here?

@benvinegar
Copy link
Contributor Author

Oddly enough, I see the rejection on raven.js:278 in the wrapped function.

@jessepollak – Raven.js wraps async methods in try/catch for uncaught exceptions (literally throw X), but that does not cover uncaught promises. The stack trace via the console still shows the wrap, though.

what's the TL;DR solution here?

@MarkMurphy – We're going to make it a config option soon-ish, but in the meantime you can implement yourself pretty easily:

https://docs.sentry.io/clients/javascript/usage/#promises

@artnez
Copy link

artnez commented Feb 10, 2017

Just a heads up for anyone coming across this thread. There is a bug in Chrome 51 and higher (as recent as Chrome 56) where unhandled promise rejections are never fired on localhost. If you're testing locally then you may never see this event fired!

See: https://bugs.chromium.org/p/v8/issues/detail?id=4874

@bendenoz
Copy link

bendenoz commented Mar 6, 2017

Just updated the issue above about Chrome.
It doesn't work for me in Chrome 56 if webpack devtool option is set to eval, which was the default setting for local development.

@akhayoon
Copy link

Just a note. If you are using Redux-Thunk with async/await, you can catch these unhandled promise rejections by catching redux-thunk errors manually.

code here https://gist.github.com/akhayoon/24be6ca9634d228893b8c4bd79f0f33c

@kamilogorek
Copy link
Contributor

It looks like the original issue has been already resolved

@avesus
Copy link

avesus commented Oct 24, 2017

@kamilogorek @benvinegar have you implemented the config option? It took a year, isn't it?

@kamilogorek
Copy link
Contributor

Hey @avesus, no, not yet, as nowadays people are using various promises implementation, which makes it hard to do.
We sure can do it natively, but then this code will be unusable by someone using Bluebird, Q or any other implementation.

For now this https://docs.sentry.io/clients/javascript/usage/#promises is what we recommend, but this may change one day.

kamilogorek pushed a commit that referenced this issue Jun 12, 2018
It's making the investigation of performance issues easier.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests