reactotron icon indicating copy to clipboard operation
reactotron copied to clipboard

Unhandled promise rejections

Open skellock opened this issue 6 years ago • 12 comments

Problem

Async error reporting is an awful dev experience.

image

We don't get a red box, we get a yellow box with a string representation of the stack trace.

So let's pipe unhandled promise rejections into Reactotron as if they were normal synchronous errors.

image

Overview

Looks like there's a few ways to do this. Each more ghetto than the next. I'm ok with this.

Since 0.44 of React Native, they started tracking unhandled exceptions as a yellow box. In their promise module, they've got some hooks to inject in your own tracking. And this is what React Native does.

Option 1

This is how React Native gets their yellow box on the screen. In the onUnhandled, they do some pretty printing and display the error. Which is pretty much never what I personally want to see.

To me, an unhandled promise rejection is an error. Full stop.

In this implementation, we just report the error (which runs the source map lookup gauntlet).

if (__DEV__) {
  require("promise/setimmediate/rejection-tracking").enable({
    allRejections: true,
    onUnhandled: (id, error = {}) => {
      console.tron.reportError(error)
    },
    onHandled: () => {},
  })
}

Option 2

The problem with the first option is that it replaces the yellow box behaviour entirely. I'm not sure I'm cool with that. We don't want to diverge too far from what RN devs expect.

This implementation will do both. It'll log the error with Reactotron and still put up the yellow box. We just swizzle good ol' _87. 🤦‍♂️

A nice side-effect of this is that we don't have to wait for the hardcoded 2 second delay on most errors that flow thru unhandled promises.

Also, I'm not sure if error trackers like mobile center and crashlytics hook these same things, but if they do, this is the safer of the two options.

if (__DEV__) {
  const old87 = Promise._87
  const new87 = (promise, err) => {
    console.tron.reportError(err)
    old87(promise, err)
  }
  Promise._87 = new87
}

Option 3

There might be other ways to do this. I'm open to suggestions.

For example, we might be able to swizzle console.warn and parse it like that? Seems like it might be a bit fragile because we'd have to do some string parsing to filter unhandled rejections.

Where To Put This

Probably right inside the track error handling function in reactotron-react-native I'd reckon.

We could likely remove the if (__DEV__) parts since that should already be protected up at the app. Reactotron shouldn't be installed in production mode unless folks really want to. In which case, they know what they're doing.

skellock avatar Dec 29 '17 12:12 skellock

so, where do we put this code?

if (__DEV__) {
  require("promise/setimmediate/rejection-tracking").enable({
    allRejections: true,
    onUnhandled: (id, error = {}) => {
      console.tron.reportError(error)
    },
    onHandled: () => {},
  })
}

jjercx avatar Oct 24 '19 16:10 jjercx

I would love to use this too! I tried Option 1 and 2 above in several different places within my RN 0.59 app with no luck. My onUnhandled() function never gets called when there's an unhandled promise rejection.

JakeStoeffler avatar Nov 01 '19 20:11 JakeStoeffler

I placed my code on my App.js, the component called by the index.js, outside the component and it really helps debugging!

jjercx avatar Nov 04 '19 21:11 jjercx

Guys, I just wanna say thanks a million for figuring out how this works internally :) I just placed the code at the top of my index.js and it works like a charm! (RN 61 btw). Definitely would love to see this incorporated in Reactotron, but for now riding steadily on the snippet 👍

abeltje1 avatar Dec 03 '19 15:12 abeltje1

Hey everyone, we just upgraded to RN 0.63.2 and this snippet stopped working, I've tried debugging it but sadly with no success, does anyone by chance know how to solve this? I'll report if I learn more

abeltje1 avatar Aug 24 '20 13:08 abeltje1

Hi all, @jjercx @JakeStoeffler @skellock, it's been a while but I believe I have found the solution to enabling this again, at least in RN 63+. In my setup, there are two issues: first of all, onUnhandled is never fired. Secondly, reportError doesn't actually report the error.

I've found this library https://github.com/iyegoroff/react-native-promise-rejection-utils, which does some more horrible internal reaching, but following his example, the onUnhandled does fire. I do not exactly know why his solution works and this one doesn't anymore, but hey, it works.

The solution for the second problem is rather easy, I copied over reactotron's code from the trackGlobalErrors plugin and had to make two little changes (see commit https://github.com/abeltje1/reactotron-react-native/commit/5fe3ecc815abd79ed7d29ff63ffe53ac64646465). I've tested it in RN 0.64.2 on iOS only, with and without Hermes enabled. Together it currently looks like this:

import {getUnhandledPromiseRejectionTracker, setUnhandledPromiseRejectionTracker} from 'react-native-promise-rejection-utils'

const prevTracker = getUnhandledPromiseRejectionTracker()
let symbolicateStackTrace
let parseErrorStack

const reportPromises = () => {
  setUnhandledPromiseRejectionTracker((id, error) => {
    parseErrorStack = parseErrorStack || require('react-native/Libraries/Core/Devtools/parseErrorStack')
    symbolicateStackTrace = symbolicateStackTrace || require('react-native/Libraries/Core/Devtools/symbolicateStackTrace')
    const parsedStacktrace = parseErrorStack(error.stack)
    symbolicateStackTrace(parsedStacktrace).then(goodStack => {
      let stack = goodStack.stack.map(stackFrame => ({
        fileName: stackFrame.file,
        functionName: stackFrame.methodName,
        lineNumber: stackFrame.lineNumber,
      }))
      console.tron.error(error.message, stack)
    })
  })
}

export default reportPromises

running this function once on startup of your app should report errors to Reactotron again! :)

Enjoy!

Cheers,

Abel

abeltje1 avatar Jul 27 '21 10:07 abeltje1

This is great, thanks! This should definitely be incorporated into the reactotron-react-native repo, but it looks basically unmaintained at this point... last time a commit was made that wasn't a dependency bump or one-off single line tweak (and even those there are only three) was over two years ago now.

finnmerlett avatar May 02 '22 23:05 finnmerlett

Hey thanks! I'm amazed it still works without issues haha. Yeah it's too bad reactotron-rn seems kinda dead, let's hope somebody picks it up again (or maybe we should do it ourselves..) :)

abeltje1 avatar May 03 '22 07:05 abeltje1

Hey thanks! I'm amazed it still works without issues haha. Yeah it's too bad reactotron-rn seems kinda dead, let's hope somebody picks it up again (or maybe we should do it ourselves..) :)

I mean fyi I haven't actually tested it, just came across the issue when wondering about implementing it, and thought it looked about right haha

finnmerlett avatar May 03 '22 17:05 finnmerlett

Embarrassed to say this, as CTO of the company that put out Reactotron, but I ran into an unhandled promise rejection in a RN app and Googled lots of solutions and eventually landed on ... this.

Yes, this should definitely be in reactotron-react-native. I'll push to get it included, but since the attention of our primary maintainers of Reactotron is elsewhere, it could be a bit. But I need it now, so.... 😅

jamonholmgren avatar May 12 '22 04:05 jamonholmgren

Can confirm this indeed works. Will try to get this fixed soon.

jamonholmgren avatar May 12 '22 05:05 jamonholmgren

Hey @jamonholmgren, that's great news (and a bit funny)! I've been steadily riding on this script for more than a year now, but would love to see it included so I can remove the horrible code from my own codebase :). One question, if you'd like to answer: what's the current priority of the maintainers of Reactotron?

abeltje1 avatar May 12 '22 07:05 abeltje1