Maciej Satkiewicz
Maciej Satkiewicz

Reputation: 163

Global unhandled promise rejection handler in React Native on production

We're developing react native app and use firebase with crashlytics, so we get informed about the app's creashes and all unhandled errors.

However we'd like to be informed about all unhandled promise rejections in our app as well - so we'd like to add global handler/listener that will send info to our server in such cases.

We already send such info in some important cases in the respective .catch() blocks, but what if we add new promises in the future and forget to handle them - then we'd want to receive info rather than not know about potential bug in our app.

Or is it a bad practice?

Upvotes: 3

Views: 2230

Answers (3)

Jack
Jack

Reputation: 131

I recently struggled a LOT with trying to find an unhandled promise rejection which turned out to be caused by me trusting the types for finally(..) too much and not providing an argument to it, when in reality the promise polyfill that react-native is using REQUIRES an argument to finally(..) or else it throws Type Error: undefined is not a function.

I did a write-up on how I identified the offending line of code by manually altering the Promise polyfill library to add a stack trace when the promise is constructed and print it out when there is an unhandled promise rejection.

You can find the write-up towards the end of this issue I posted. Good luck and let me know if it helps!

EDIT 1: I just realized I was about to get hate for not providing the answer inline here so I have copied it from the issue here, apologies if the context does not fit exactly. Also this solution is for one particular promise polyfill library, but I imagine the same ideas could be used for other libraries where you want to track down the stack trace of the original promise when it is marked as unhandled.

Identifying the offending line of code

I tried a few different StackOverflow answers for trying to find the real stack trace of the promise that was causing the issue I was seeing. As noted the stack trace that is printed does not end up leading to any code I have written, which made it questionable whether it was my fault or potentially some other library causing the issue.

After trying to use bluebird as suggested here I curiously found that the warning went away. Now I know it was because Bluebird promise implementation does not require a finally parameter!

In the end, I did the following to identify the offending line of code (sorry this isn't a patch-package patch as we do not use that lib) I manually added three lines in two files.

File 1: node_modules/promise/setimmediate/core.js Add the following line of code in the function Promise(fn) {... function (line 61 for me).

  this._stack = (new Error()).stack;

File 2: node_modules/promise/setimmediate/rejection-tracking.js Inside the enable function where a setTimeout is called (line 47 for me) with:

onUnhandled.bind(null, promise._51)

Replace it with:

onUnhandled.bind(null, promise._51, promise)

Then in the onUnhandled function just add a 2nd parameter "promise" and console log there and you'll see the _stack field printed out with the original location where the Promise was created.

  function onUnhandled(id, promise) { // line 60 for me
    console.log("rejection-tracking:61 - onUnhandled", id, promise)

Although I got a bundled stack trace I was still able to track it down when the code was not minified.

NOTE: This is NOT performant and SHOULD NOT be included in any production builds as calling (new Error()).stack on every promise constructor will slow things down a lot. For debugging this is not an issue though.

I'd love for someone to tell me how I could have done this an alternative way as I feel it is ridiculous I had to go through these steps.

Upvotes: 1

Hamza Waleed
Hamza Waleed

Reputation: 1462

You can use componentDidCatch for this purpose. You can wrap top level route components, you can also wrap components that might contain obscure code. It's up to you how to best handle application crashes.

componentDidCatch(error, errorInfo){}`

The first method parameter is the actual error thrown. The second parameter is an object with a componentStack property containing the component stack trace information.

Upvotes: 1

iyegoroff
iyegoroff

Reputation: 591

To track unhandled promise rejections globally you can use my react-native-promise-rejection-utils package:

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

const prevTracker = getUnhandledPromiseRejectionTracker()

setUnhandledPromiseRejectionTracker((id, error) => {
  console.warn('Unhandled promise rejection!', id, error)

  if (prevTracker !== undefined) {
    prevTracker(id, error)
  }
})

Or is it a bad practice?

There is always a chance to receive an unexpected error that is not described in the business logic of your app (e.g. an exception thrown by some third-party library). Logging an unexpected error globally is a good practice, because it lets you know that such error exists at all.

Upvotes: 3

Related Questions