├── .eslintrc ├── .gitattributes ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── ReactNativeExceptionHandler.podspec ├── android ├── build.gradle └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── masteratul │ │ └── exceptionhandler │ │ ├── DefaultErrorScreen.java │ │ ├── NativeExceptionHandlerIfc.java │ │ ├── ReactNativeExceptionHandlerModule.java │ │ └── ReactNativeExceptionHandlerPackage.java │ └── res │ ├── layout │ └── default_error_screen.xml │ └── values │ └── strings.xml ├── examples ├── bugCaptureOnError.js ├── preservingOldHandler.js └── restartOnError.js ├── index.d.ts ├── index.js ├── ios ├── ReactNativeExceptionHandler.h ├── ReactNativeExceptionHandler.m └── ReactNativeExceptionHandler.xcodeproj │ └── project.pbxproj ├── package.json ├── screens ├── WITHOUT_DEV.gif ├── WITHOUT_PROD.gif ├── WITH_EH.gif ├── android_native_exception.png └── ios_native_exception.png └── yarn.lock /.eslintrc: -------------------------------------------------------------------------------- 1 | # "off" or 0 - turn the rule off 2 | # "warn" or 1 - turn the rule on as a warning(doesn’ t affect exit code) 3 | # "error" or 2 - turn the rule on as an error(exit code is 1 when triggered) 4 | { 5 | "parser": "babel-eslint", 6 | "env": { 7 | "browser": true 8 | }, 9 | "plugins": [ 10 | "react", 11 | "react-native" 12 | ], 13 | "parserOptions": { 14 | "ecmaFeatures": { 15 | "jsx": true 16 | }, 17 | "sourceType": "module" 18 | }, 19 | "extends": ["eslint:recommended", "plugin:react/recommended"], 20 | "rules": { 21 | "react/no-did-mount-set-state": 2, 22 | "react/no-did-update-set-state": 2, 23 | "react/no-direct-mutation-state": 2, 24 | "react/jsx-uses-vars": 2, 25 | "no-unused-vars": ["error", { 26 | "varsIgnorePattern": "React" 27 | }], 28 | "comma-spacing": ["error", { 29 | "before": false, 30 | "after": true }], 31 | "no-undef": 2, 32 | "semi": 2, 33 | "react/prop-types": 2, 34 | "react/jsx-no-bind": 2, 35 | "react/jsx-no-duplicate-props": 2, 36 | "space-before-blocks": 2, 37 | "space-before-function-paren": 2, 38 | "space-in-parens": 2, 39 | "space-infix-ops": 2, 40 | "space-unary-ops": 2, 41 | "spaced-comment": 2, 42 | "rest-spread-spacing": 2, 43 | "semi-spacing": 2, 44 | "no-unneeded-ternary": 2, 45 | "eqeqeq": 2, 46 | "dot-location": 2, 47 | "no-extra-bind": 2, 48 | "keyword-spacing": 2, 49 | "key-spacing": 2, 50 | "indent": ["error", 2], 51 | "react/jsx-indent": [2, 2], 52 | "func-call-spacing": 2, 53 | "array-bracket-spacing": 2, 54 | "block-spacing": 2, 55 | "brace-style": 2, 56 | "arrow-body-style": 2, 57 | "arrow-parens": 2, 58 | "arrow-spacing": 2, 59 | "react/self-closing-comp": 2, 60 | "jsx-quotes": ["error", "prefer-single"], 61 | "object-curly-spacing": 2, 62 | "quotes": ["error", "single"], 63 | "no-console": 0 64 | }, 65 | "globals": { 66 | "global": false, 67 | "it": false, 68 | "xit": false, 69 | "expect": false, 70 | "describe": false, 71 | "require": false, 72 | "module": false, 73 | "Promise": false, 74 | "__DEV__": false 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.pbxproj -text -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # OSX 3 | # 4 | .DS_Store 5 | 6 | # node.js 7 | # 8 | node_modules/ 9 | npm-debug.log 10 | yarn-error.log 11 | 12 | 13 | # Xcode 14 | # 15 | build/ 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | xcuserdata 25 | *.xccheckout 26 | *.moved-aside 27 | DerivedData 28 | *.hmap 29 | *.ipa 30 | *.xcuserstate 31 | project.xcworkspace 32 | 33 | 34 | # Android/IntelliJ 35 | # 36 | build/ 37 | .idea 38 | .gradle 39 | local.properties 40 | *.iml 41 | 42 | # BUCK 43 | buck-out/ 44 | \.buckd/ 45 | *.keystore 46 | 47 | 48 | node_modules 49 | .git 50 | cache 51 | npm-debug.log 52 | coverage 53 | .DS_Store 54 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | 2 | # OSX 3 | # 4 | .DS_Store 5 | 6 | # node.js 7 | # 8 | node_modules/ 9 | npm-debug.log 10 | yarn-error.log 11 | 12 | 13 | # Xcode 14 | # 15 | build/ 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | xcuserdata 25 | *.xccheckout 26 | *.moved-aside 27 | DerivedData 28 | *.hmap 29 | *.ipa 30 | *.xcuserstate 31 | project.xcworkspace 32 | 33 | 34 | # Android/IntelliJ 35 | # 36 | build/ 37 | .idea 38 | .gradle 39 | local.properties 40 | *.iml 41 | 42 | # BUCK 43 | buck-out/ 44 | \.buckd/ 45 | *.keystore 46 | 47 | 48 | node_modules 49 | .git 50 | cache 51 | npm-debug.log 52 | coverage 53 | .DS_Store 54 | 55 | screens 56 | android/build 57 | examples 58 | README.md 59 | .eslintrc -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Atul 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-native-exception-handler ![npm](https://img.shields.io/npm/dm/react-native-exception-handler.svg) 2 | 3 | [![https://nodei.co/npm/react-native-exception-handler.png?downloads=true&downloadRank=true&stars=true](https://nodei.co/npm/react-native-exception-handler.png?downloads=true&downloadRank=true&stars=true)](https://www.npmjs.com/package/react-native-exception-handler) 4 | 5 | A react native module that lets you to register a global error handler that can capture fatal/non fatal uncaught exceptions. 6 | The module helps prevent abrupt crashing of RN Apps without a graceful message to the user. 7 | 8 | In the current scenario: 9 | 10 | - `In DEV mode , you get a RED Screen error pointing your errors.` 11 | - `In Bundled mode , the app just quits without any prompt !` 🙄 12 | 13 | To tackle this we register a global error handler that could be used to for example: 14 | 15 | 1. Send bug reports to dev team when the app crashes 16 | 2. Show a creative dialog saying the user should restart the application 17 | 18 | #### UPDATE - V2: 19 | 20 | **V2 of this module now supports catching Unhandled Native Exceptions also along with the JS Exceptions ✌🏻🍻** 21 | There are **NO** breaking changes. So its safe to upgrade from v1 to v2. So there is no reason not to 😉. 22 | 23 | **V2.9** 24 | 25 | - Adds support for executing previously set error handlers (now this module can work with other analytics modules) 26 | - Adds an improved approach for overwriting native error handlers. 27 | - Thanks @ [Damien Solimando](https://github.com/dsolimando) 28 | 29 | **Example** repo can be found here: 30 | _[https://github.com/master-atul/react-native-exception-handler-example](https://github.com/master-atul/react-native-exception-handler-example) _ 31 | 32 | ### Screens 33 | 34 | ##### Without any error handling 35 | 36 | **In DEV MODE** 37 | 38 |
39 | 40 |
41 | 42 |
43 |
44 | 45 | **In BUNDLED MODE** 46 | 47 |
48 | 49 |
50 | 51 |
52 |
53 | 54 | **With react-native-exception-handler in BUNDLED MODE** 55 | 56 |
57 | 58 |
59 | 60 |
61 |
62 | 63 | ### Installation: 64 | 65 | `yarn add react-native-exception-handler` 66 | 67 | or 68 | 69 | `npm i react-native-exception-handler --save` 70 | 71 | ### Mostly automatic installation 72 | 73 | `react-native link react-native-exception-handler` 74 | 75 | ### For react-native@0.60.0 or above 76 | 77 | As [react-native@0.60.0](https://reactnative.dev/blog/2019/07/03/version-60) or above supports autolinking, so there is no need to run linking process. 78 | Read more about autolinking [here](https://github.com/react-native-picker/cli/blob/master/docs/autolinking.md). 79 | 80 | ### Manual installation 81 | 82 | #### iOS 83 | 84 | 1. In XCode, in the project navigator, right click `Libraries` ➜ `Add Files to [your project's name]` 85 | 2. Go to `node_modules` ➜ `react-native-exception-handler` and add `ReactNativeExceptionHandler.xcodeproj` 86 | 3. In XCode, in the project navigator, select your project. Add `libReactNativeExceptionHandler.a` to your project's `Build Phases` ➜ `Link Binary With Libraries` 87 | 4. Run your project (`Cmd+R`)< 88 | 89 | ##### Using Cocoapods 90 | 91 | 1. add `pod 'ReactNativeExceptionHandler', :podspec => '../node_modules/react-native-exception-handler/ReactNativeExceptionHandler.podspec'` to your Podfile 92 | 2. run `pod install` 93 | 94 | #### Android 95 | 96 | 1. Open up `android/app/src/main/java/[...]/MainApplication.java` 97 | 98 | - Add `import com.masteratul.exceptionhandler.ReactNativeExceptionHandlerPackage;` to the imports at the top of the file 99 | - Add `new ReactNativeExceptionHandlerPackage()` to the list returned by the `getPackages()` method 100 | 101 | 2. Append the following lines to `android/settings.gradle`: 102 | ``` 103 | include ':react-native-exception-handler' 104 | project(':react-native-exception-handler').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-exception-handler/android') 105 | ``` 106 | 3. Insert the following lines inside the dependencies block in `android/app/build.gradle`: 107 | ``` 108 | compile project(':react-native-exception-handler') 109 | ``` 110 | 111 | ### PLEASE READ BEFORE GOING TO USAGE SECTION 112 | 113 | Lets introduce you to the type of errors in a RN app. 114 | 115 | - Errors produced by your Javascript code (includes all your react code). We will refer to these errors as **JS_Exceptions** going forward. 116 | 117 | - Errors produced by Native Modules. We will refer to these as **Native_Exceptions** going forward. 118 | 119 | Unhandled exceptions leave the app in a critical state. 120 | 121 | In case of **JS_Exceptions** you can catch these unhandled exceptions and perform tasks like show alerts or popups, do cleanup or even hit an API to inform the dev teams before closing the app. 122 | 123 | In case of **Native_Exceptions** it becomes much worse. While you can catch these unhandled exceptions and perform tasks like cleanup or logout or even hit an API to inform the dev teams before closing the app, 124 | you CANNOT show a JS alert box or do any UI stuff via JS code. This has to be done via the native methods provided by this module in the respective NATIVE codebase for iOS and android. The module does provide default handlers though :P. So you will get default popups incase of errors. Obviously you can customise them. See CUSTOMIZATION section. 125 | 126 | ### Usage 127 | 128 | To catch **JS_Exceptions** 129 | 130 | ```js 131 | import {setJSExceptionHandler, getJSExceptionHandler} from 'react-native-exception-handler'; 132 | 133 | . 134 | . 135 | // For most use cases: 136 | // registering the error handler (maybe u can do this in the index.android.js or index.ios.js) 137 | setJSExceptionHandler((error, isFatal) => { 138 | // This is your custom global error handler 139 | // You do stuff like show an error dialog 140 | // or hit google analytics to track crashes 141 | // or hit a custom api to inform the dev team. 142 | }); 143 | //================================================= 144 | // ADVANCED use case: 145 | const exceptionhandler = (error, isFatal) => { 146 | // your error handler function 147 | }; 148 | setJSExceptionHandler(exceptionhandler, allowInDevMode); 149 | // - exceptionhandler is the exception handler function 150 | // - allowInDevMode is an optional parameter is a boolean. 151 | // If set to true the handler to be called in place of RED screen 152 | // in development mode also. 153 | 154 | // getJSExceptionHandler gives the currently set JS exception handler 155 | const currentHandler = getJSExceptionHandler(); 156 | ``` 157 | 158 | To catch **Native_Exceptions** 159 | 160 | ```js 161 | import { setNativeExceptionHandler } from "react-native-exception-handler"; 162 | 163 | //For most use cases: 164 | setNativeExceptionHandler((exceptionString) => { 165 | // This is your custom global error handler 166 | // You do stuff likehit google analytics to track crashes. 167 | // or hit a custom api to inform the dev team. 168 | //NOTE: alert or showing any UI change via JS 169 | //WILL NOT WORK in case of NATIVE ERRORS. 170 | }); 171 | //==================================================== 172 | // ADVANCED use case: 173 | const exceptionhandler = (exceptionString) => { 174 | // your exception handler code here 175 | }; 176 | setNativeExceptionHandler( 177 | exceptionhandler, 178 | forceAppQuit, 179 | executeDefaultHandler 180 | ); 181 | // - exceptionhandler is the exception handler function 182 | // - forceAppQuit is an optional ANDROID specific parameter that defines 183 | // if the app should be force quit on error. default value is true. 184 | // To see usecase check the common issues section. 185 | // - executeDefaultHandler is an optional boolean (both IOS, ANDROID) 186 | // It executes previous exception handlers if set by some other module. 187 | // It will come handy when you use any other crash analytics module along with this one 188 | // Default value is set to false. Set to true if you are using other analytics modules. 189 | ``` 190 | 191 | It is recommended you set both the handlers. 192 | **NOTE: `setNativeExceptionHandler` only works in bundled mode - it will show the red screen when applied to dev mode.** 193 | 194 | **See the examples to know more** 195 | 196 | ### CUSTOMIZATION 197 | 198 | #### Customizing **setJSExceptionHandler**. 199 | 200 | In case of `setJSExceptionHandler` you can do everything that is possible. Hence there is not much to customize here. 201 | 202 | ```js 203 | const errorHandler = (error, isFatal) => { 204 | // This is your custom global error handler 205 | // You do stuff like show an error dialog 206 | // or hit google analytics to track crashes 207 | // or hit a custom api to inform the dev team. 208 | }) 209 | //Second argument is a boolean with a default value of false if unspecified. 210 | //If set to true the handler to be called in place of RED screen 211 | //in development mode also. 212 | setJSExceptionHandler(errorHandler, true); 213 | ``` 214 | 215 | #### Customizing **setNativeExceptionHandler** 216 | 217 | By default whenever a **Native_Exceptions** occurs if you have used `setNativeExceptionHandler`, **along with the callback specified** you would see a popup (this is the default native handler set by this module). 218 | 219 | In Android and iOS you will see something like 220 | 221 |

222 | 223 | 224 |

225 | 226 | **Modifying Android Native Exception handler (RECOMMENDED APPROACH)** 227 | 228 | (NATIVE CODE HAS TO BE WRITTEN) _recommended that you do this in android studio_ 229 | 230 | - In the `android/app/src/main/java/[...]/MainApplication.java` 231 | 232 | ```java 233 | import com.masteratul.exceptionhandler.ReactNativeExceptionHandlerModule; 234 | import com.masteratul.exceptionhandler.NativeExceptionHandlerIfc 235 | ... 236 | ... 237 | ... 238 | public class MainApplication extends Application implements ReactApplication { 239 | ... 240 | ... 241 | @Override 242 | public void onCreate() { 243 | .... 244 | .... 245 | .... 246 | ReactNativeExceptionHandlerModule.setNativeExceptionHandler(new NativeExceptionHandlerIfc() { 247 | @Override 248 | public void handleNativeException(Thread thread, Throwable throwable, Thread.UncaughtExceptionHandler originalHandler) { 249 | // Put your error handling code here 250 | } 251 | });//This will override the default behaviour of displaying the recover activity. 252 | } 253 | ``` 254 | 255 | **Modifying Android Native Exception handler UI (CUSTOM ACTIVITY APPROACH (OLD APPROACH).. LEAVING FOR BACKWARD COMPATIBILITY)** 256 | 257 | (NATIVE CODE HAS TO BE WRITTEN) _recommended that you do this in android studio_ 258 | 259 | - Create an Empty Activity in the `android/app/src/main/java/[...]/`. For example lets say CustomErrorDialog.java 260 | - Customize your activity to look and behave however you need it to be. 261 | - In the `android/app/src/main/java/[...]/MainApplication.java` 262 | 263 | ```java 264 | import com.masteratul.exceptionhandler.ReactNativeExceptionHandlerModule; 265 | import .YourCustomActivity; //This is your CustomErrorDialog.java 266 | ... 267 | ... 268 | ... 269 | public class MainApplication extends Application implements ReactApplication { 270 | ... 271 | ... 272 | @Override 273 | public void onCreate() { 274 | .... 275 | .... 276 | .... 277 | ReactNativeExceptionHandlerModule.replaceErrorScreenActivityClass(YourCustomActivity.class); //This will replace the native error handler popup with your own custom activity. 278 | } 279 | ``` 280 | 281 | **Modifying iOS Native Exception handler UI** (NATIVE CODE HAS TO BE WRITTEN) _recommended that you do this in XCode_ 282 | 283 | Unlike Android, in the case of iOS, there is no way to restart the app if it has crashed. Also, during a **Native_Exceptions** the UI becomes quite unstable since the exception occured on the main UI thread. Hence, none of the click or press handlers would work either. 284 | 285 | Keeping in mind of these, at the most we can just show the user a dialog and inform the user to reopen the app. 286 | 287 | If you noticed the default native exception popup does exactly that. To customize the UI for the popup. 288 | 289 | - In XCode, open the file `AppDelegate.m` 290 | 291 | ```c 292 | #import "AppDelegate.h" 293 | 294 | #import 295 | #import 296 | 297 | //Add the header file 298 | #import "ReactNativeExceptionHandler.h" 299 | ... 300 | ... 301 | @implementation AppDelegate 302 | 303 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 304 | { 305 | ... 306 | ... 307 | 308 | [ReactNativeExceptionHandler replaceNativeExceptionHandlerBlock:^(NSException *exception, NSString *readeableException){ 309 | 310 | // THE CODE YOU WRITE HERE WILL REPLACE THE EXISTING NATIVE POPUP THAT COMES WITH THIS MODULE. 311 | //We create an alert box 312 | UIAlertController* alert = [UIAlertController 313 | alertControllerWithTitle:@"Critical error occurred" 314 | message: [NSString stringWithFormat:@"%@\n%@", 315 | @"Apologies..The app will close now \nPlease restart the app\n", 316 | readeableException] 317 | preferredStyle:UIAlertControllerStyleAlert]; 318 | 319 | // We show the alert box using the rootViewController 320 | [rootViewController presentViewController:alert animated:YES completion:nil]; 321 | 322 | // THIS IS THE IMPORTANT PART 323 | // By default when an exception is raised we will show an alert box as per our code. 324 | // But since our buttons wont work because our click handlers wont work. 325 | // to close the app or to remove the UI lockup on exception. 326 | // we need to call this method 327 | // [ReactNativeExceptionHandler releaseExceptionHold]; // to release the lock and let the app crash. 328 | 329 | // Hence we set a timer of 4 secs and then call the method releaseExceptionHold to quit the app after 330 | // 4 secs of showing the popup 331 | [NSTimer scheduledTimerWithTimeInterval:4.0 332 | target:[ReactNativeExceptionHandler class] 333 | selector:@selector(releaseExceptionHold) 334 | userInfo:nil 335 | repeats:NO]; 336 | 337 | // or you can call 338 | // [ReactNativeExceptionHandler releaseExceptionHold]; when you are done to release the UI lock. 339 | }]; 340 | 341 | ... 342 | ... 343 | ... 344 | 345 | return YES; 346 | } 347 | 348 | @end 349 | ``` 350 | 351 | What is this `[ReactNativeExceptionHandler releaseExceptionHold];`? 352 | 353 | In case of iOS we lock the UI thread after we show our popup to prevent the app from closing. 354 | Hence once we are done showing the popup we need to close our app after some time. 355 | But since our buttons wont work as our click handlers wont work (explained before). 356 | To close the app or to remove the UI lockup on exception, we need to call this method 357 | `[ReactNativeExceptionHandler releaseExceptionHold];` 358 | 359 | Hence we set a timer of 4 secs and then call the method releaseExceptionHold to quit the app after 360 | 4 secs of showing the popup 361 | 362 | ```c 363 | [NSTimer scheduledTimerWithTimeInterval:4.0 364 | target:[ReactNativeExceptionHandler class] 365 | selector:@selector(releaseExceptionHold) 366 | userInfo:nil 367 | repeats:NO]; 368 | ``` 369 | 370 | ### Examples 371 | 372 | ##### Restart on error example 373 | 374 | This example shows how to use this module show a graceful bug dialog to the user on crash and restart the app when the user presses ok ! 375 | 376 | ```js 377 | import {Alert} from 'react-native'; 378 | import RNRestart from 'react-native-restart'; 379 | import {setJSExceptionHandler} from 'react-native-exception-handler'; 380 | 381 | const errorHandler = (e, isFatal) => { 382 | if (isFatal) { 383 | Alert.alert( 384 | 'Unexpected error occurred', 385 | ` 386 | Error: ${(isFatal) ? 'Fatal:' : ''} ${e.name} ${e.message} 387 | 388 | We will need to restart the app. 389 | `, 390 | [{ 391 | text: 'Restart', 392 | onPress: () => { 393 | RNRestart.Restart(); 394 | } 395 | }] 396 | ); 397 | } else { 398 | console.log(e); // So that we can see it in the ADB logs in case of Android if needed 399 | } 400 | }; 401 | 402 | setJSExceptionHandler(errorHandler); 403 | 404 | setNativeExceptionHandler((errorString) => { 405 | //You can do something like call an api to report to dev team here 406 | ... 407 | ... 408 | // When you call setNativeExceptionHandler, react-native-exception-handler sets a 409 | // Native Exception Handler popup which supports restart on error in case of android. 410 | // In case of iOS, it is not possible to restart the app programmatically, so we just show an error popup and close the app. 411 | // To customize the popup screen take a look at CUSTOMIZATION section. 412 | }); 413 | ``` 414 | 415 | #### Bug Capture to dev team example 416 | 417 | This example shows how to use this module to send global errors to the dev team and show a graceful bug dialog to the user on crash ! 418 | 419 | ```js 420 | import { Alert } from "react-native"; 421 | import { BackAndroid } from "react-native"; 422 | import { setJSExceptionHandler } from "react-native-exception-handler"; 423 | 424 | const reporter = (error) => { 425 | // Logic for reporting to devs 426 | // Example : Log issues to github issues using github apis. 427 | console.log(error); // sample 428 | }; 429 | 430 | const errorHandler = (e, isFatal) => { 431 | if (isFatal) { 432 | reporter(e); 433 | Alert.alert( 434 | "Unexpected error occurred", 435 | ` 436 | Error: ${isFatal ? "Fatal:" : ""} ${e.name} ${e.message} 437 | 438 | We have reported this to our team ! Please close the app and start again! 439 | `, 440 | [ 441 | { 442 | text: "Close", 443 | onPress: () => { 444 | BackAndroid.exitApp(); 445 | }, 446 | }, 447 | ] 448 | ); 449 | } else { 450 | console.log(e); // So that we can see it in the ADB logs in case of Android if needed 451 | } 452 | }; 453 | 454 | setJSExceptionHandler(errorHandler); 455 | 456 | setNativeExceptionHandler((errorString) => { 457 | //You can do something like call an api to report to dev team here 458 | //example 459 | // fetch('http://?error='+errorString); 460 | // 461 | }); 462 | ``` 463 | 464 | _More Examples can be found in the examples folder_ 465 | 466 | - Preserving old handler (thanks to zeh) 467 | 468 | # Known issues and fixes: 469 | 470 | ### react-native-navigation (Wix) 471 | 472 | This is specifically occuring when you use [wix library](http://wix.github.io/react-native-navigation/) for navigation along with react-native-exception-handler. Whenever an error occurs, it will recreate the application above the crash screen. 473 | 474 | **Fix:** 475 | 476 | You need to set second parametera as _false_ while calling _setNativeExceptionHandler_. 477 | The second parameter is an android specific field which stands for forceQuitOnError. 478 | When set to false it doesnt quit the app forcefully on error. In short : 479 | 480 | Credit goes to **Gustavo Fão Valvassori** 481 | 482 | ```js 483 | setNativeExceptionHandler(nativeErrorCallback, false); 484 | ``` 485 | 486 | ### Previously defined exception handlers are not executed anymore 487 | 488 | A lot of frameworks (especially analytics sdk's) implement global exception handlers. In order to keep these frameworks working while using react-native-exception-hanlder, you can pass a boolean value as third argument to `setNativeExceptionHandler(..., ..., true`) what will trigger the execution of the last global handler registered. 489 | 490 | ## CONTRIBUTORS 491 | 492 | - [Atul R](https://github.com/master-atul) 493 | - [Zeh Fernando](https://github.com/zeh) 494 | - [Fred Chasen](https://github.com/fchasen) 495 | - [Christoph Jerolimov](https://github.com/jerolimov) 496 | - [Peter Chow](https://github.com/peteroid) 497 | - [Gustavo Fão Valvassori](https://github.com/faogustavo) 498 | - [Alessandro Agosto](https://github.com/lexor90) 499 | - [robinxb](https://github.com/robinxb) 500 | - [Gant Laborde](https://github.com/GantMan) 501 | - [Himanshu Singh](https://github.com/himanshusingh2407) 502 | - [Paulus Esterhazy](https://github.com/pesterhazy) 503 | - [TomMahle](https://github.com/TomMahle) 504 | - [Sébastien Krafft](https://github.com/skrafft) 505 | - [Mark Friedman](https://github.com/mark-friedman) 506 | - [Damien Solimando](https://github.com/dsolimando) 507 | - [Jens Kuhr Jørgensen](https://github.com/jenskuhrjorgensen) 508 | - [Szabó Zsolt](https://github.com/alexovits) 509 | - [Andrew Vyazovoy](https://github.com/Vyazovoy) 510 | - [Pierre Segalen](https://github.com/psegalen) 511 | - [Denis Slávik](https://github.com/slavikdenis) 512 | 513 | ## TESTING NATIVE EXCEPTIONS/ERRORS 514 | 515 | To make sure this module works. You can generate a native exception using the module `rn-test-exception-handler`. 516 | [https://github.com/master-atul/rn-test-exception-handler](https://github.com/master-atul/rn-test-exception-handler) 517 | 518 | `rn-test-exception-handler` module does only one thing. It raises a **Native_Exceptions**. 519 | This will help you to verify your customizations or functionality of this module. 520 | 521 | Peace ! ✌🏻🍻 522 | -------------------------------------------------------------------------------- /ReactNativeExceptionHandler.podspec: -------------------------------------------------------------------------------- 1 | require 'json' 2 | 3 | # Returns the version number for a package.json file 4 | pkg_version = lambda do |dir_from_root = '', version = 'version'| 5 | path = File.join(__dir__, dir_from_root, 'package.json') 6 | JSON.parse(File.read(path))[version] 7 | end 8 | 9 | # Let the main package.json decide the version number for the pod 10 | package_version = pkg_version.call 11 | 12 | Pod::Spec.new do |s| 13 | s.name = "ReactNativeExceptionHandler" 14 | s.version = package_version 15 | s.summary = "A react native module that lets you to register a global error handler that can capture fatal/non fatal uncaught exceptions" 16 | s.description = <<-DESC 17 | A react native module that lets you to register a global error handler that can capture fatal/non fatal uncaught exceptions. 18 | The module helps prevent abrupt crashing of RN Apps without a graceful message to the user. 19 | DESC 20 | s.homepage = "https://github.com/master-atul/react-native-exception-handler" 21 | s.license = "MIT" 22 | s.author = { "Atul R" => "atulanand94@gmail.com" } 23 | s.platform = :ios, "9.0" 24 | s.source = { :git => "https://github.com/master-atul/react-native-exception-handler.git", :tag => s.version.to_s } 25 | s.source_files = "ios/*.{h,m}" 26 | 27 | s.dependency "React-Core" 28 | 29 | end 30 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | 2 | buildscript { 3 | repositories { 4 | jcenter() 5 | } 6 | 7 | dependencies { 8 | 9 | } 10 | } 11 | 12 | apply plugin: 'com.android.library' 13 | 14 | 15 | 16 | def getExtValue(rootProject,key,defaultValue ) { 17 | if (rootProject.hasProperty('ext')) { 18 | if (rootProject.ext.has(key)) { 19 | return rootProject.ext[key] 20 | } 21 | } 22 | return defaultValue 23 | } 24 | 25 | android { 26 | compileSdkVersion getExtValue(rootProject,'compileSdkVersion',23) 27 | buildToolsVersion getExtValue(rootProject,'buildToolsVersion', "23.0.1") 28 | 29 | defaultConfig { 30 | minSdkVersion getExtValue(rootProject,'minSdkVersion', 16) 31 | targetSdkVersion getExtValue(rootProject,'targetSdkVersion', 22) 32 | versionCode 1 33 | versionName "1.0" 34 | } 35 | lintOptions { 36 | abortOnError false 37 | } 38 | } 39 | 40 | repositories { 41 | mavenCentral() 42 | } 43 | 44 | dependencies { 45 | implementation 'com.facebook.react:react-native:+' 46 | } 47 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /android/src/main/java/com/masteratul/exceptionhandler/DefaultErrorScreen.java: -------------------------------------------------------------------------------- 1 | package com.masteratul.exceptionhandler; 2 | 3 | import android.app.Activity; 4 | import android.app.AlarmManager; 5 | import android.app.PendingIntent; 6 | import android.content.Context; 7 | import android.content.Intent; 8 | import android.content.pm.PackageManager; 9 | import android.os.Bundle; 10 | import android.util.Log; 11 | import android.view.View; 12 | import android.widget.Button; 13 | import android.widget.TextView; 14 | 15 | public class DefaultErrorScreen extends Activity { 16 | private static String TAG = "RN_ERROR_HANDLER"; 17 | private Button quitButton; 18 | private Button relaunchButton; 19 | private Button showDetailsButton; 20 | private TextView stackTraceView; 21 | 22 | @Override 23 | protected void onCreate(Bundle savedInstanceState) { 24 | super.onCreate(savedInstanceState); 25 | String stackTraceString = "StackTrace unavailable"; 26 | try { 27 | stackTraceString = getIntent().getExtras().getString("stack_trace_string"); 28 | } 29 | catch (Exception e) { 30 | Log.e(TAG, String.format("Was not able to get StackTrace: %s", e.getMessage())); 31 | } 32 | setContentView(R.layout.default_error_screen); 33 | quitButton = (Button) findViewById(R.id.eh_quit_button); 34 | relaunchButton = (Button) findViewById(R.id.eh_restart_button); 35 | showDetailsButton = (Button) findViewById(R.id.eh_show_details_button); 36 | stackTraceView = (TextView) findViewById(R.id.eh_stack_trace_text_view); 37 | stackTraceView.setText(stackTraceString); 38 | 39 | showDetailsButton.setOnClickListener(new View.OnClickListener() { 40 | @Override 41 | public void onClick(View view) { 42 | int stackTraceViewVisibility = stackTraceView.getVisibility(); 43 | if(stackTraceViewVisibility == View.VISIBLE){ 44 | stackTraceView.setVisibility(View.GONE); 45 | showDetailsButton.setText("SHOW DETAILS"); 46 | }else{ 47 | stackTraceView.setVisibility(View.VISIBLE); 48 | showDetailsButton.setText("HIDE DETAILS"); 49 | } 50 | } 51 | }); 52 | 53 | relaunchButton.setOnClickListener(new View.OnClickListener() { 54 | @Override 55 | public void onClick(View view) { 56 | doRestart(getApplicationContext()); 57 | } 58 | }); 59 | 60 | quitButton.setOnClickListener(new View.OnClickListener() { 61 | @Override 62 | public void onClick(View view) { 63 | System.exit(0); 64 | } 65 | }); 66 | } 67 | 68 | public static void doRestart(Context c) { 69 | try { 70 | if (c != null) { 71 | PackageManager pm = c.getPackageManager(); 72 | if (pm != null) { 73 | Intent mStartActivity = pm.getLaunchIntentForPackage( 74 | c.getPackageName() 75 | ); 76 | if (mStartActivity != null) { 77 | mStartActivity.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); 78 | int mPendingIntentId = 654311; 79 | PendingIntent mPendingIntent = PendingIntent 80 | .getActivity(c, mPendingIntentId, mStartActivity, 81 | PendingIntent.FLAG_CANCEL_CURRENT); 82 | AlarmManager mgr = (AlarmManager) c.getSystemService(Context.ALARM_SERVICE); 83 | mgr.set(AlarmManager.RTC, System.currentTimeMillis() + 100, mPendingIntent); 84 | System.exit(0); 85 | } else { 86 | throw new Exception("Was not able to restart application, mStartActivity null"); 87 | } 88 | } else { 89 | throw new Exception("Was not able to restart application, PM null"); 90 | } 91 | } else { 92 | throw new Exception("Was not able to restart application, Context null"); 93 | } 94 | } catch (Exception ex) { 95 | Log.e(TAG, "Was not able to restart application"); 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /android/src/main/java/com/masteratul/exceptionhandler/NativeExceptionHandlerIfc.java: -------------------------------------------------------------------------------- 1 | package com.masteratul.exceptionhandler; 2 | 3 | public interface NativeExceptionHandlerIfc { 4 | void handleNativeException(Thread thread, Throwable throwable, Thread.UncaughtExceptionHandler originalHandler); 5 | } 6 | -------------------------------------------------------------------------------- /android/src/main/java/com/masteratul/exceptionhandler/ReactNativeExceptionHandlerModule.java: -------------------------------------------------------------------------------- 1 | 2 | package com.masteratul.exceptionhandler; 3 | 4 | import android.app.Activity; 5 | import android.content.Intent; 6 | import android.util.Log; 7 | 8 | import com.facebook.react.bridge.Callback; 9 | import com.facebook.react.bridge.ReactApplicationContext; 10 | import com.facebook.react.bridge.ReactContextBaseJavaModule; 11 | import com.facebook.react.bridge.ReactMethod; 12 | 13 | public class ReactNativeExceptionHandlerModule extends ReactContextBaseJavaModule { 14 | 15 | private ReactApplicationContext reactContext; 16 | private Activity activity; 17 | private static Class errorIntentTargetClass = DefaultErrorScreen.class; 18 | private static NativeExceptionHandlerIfc nativeExceptionHandler; 19 | private Callback callbackHolder; 20 | private Thread.UncaughtExceptionHandler originalHandler; 21 | 22 | public ReactNativeExceptionHandlerModule(ReactApplicationContext reactContext) { 23 | super(reactContext); 24 | this.reactContext = reactContext; 25 | } 26 | 27 | @Override 28 | public String getName() { 29 | return "ReactNativeExceptionHandler"; 30 | } 31 | 32 | 33 | @ReactMethod 34 | public void setHandlerforNativeException( 35 | final boolean executeOriginalUncaughtExceptionHandler, 36 | final boolean forceToQuit, 37 | Callback customHandler) { 38 | 39 | callbackHolder = customHandler; 40 | originalHandler = Thread.getDefaultUncaughtExceptionHandler(); 41 | 42 | Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { 43 | 44 | @Override 45 | public void uncaughtException(Thread thread, Throwable throwable) { 46 | 47 | String stackTraceString = Log.getStackTraceString(throwable); 48 | callbackHolder.invoke(stackTraceString); 49 | 50 | if (nativeExceptionHandler != null) { 51 | nativeExceptionHandler.handleNativeException(thread, throwable, originalHandler); 52 | } else { 53 | activity = getCurrentActivity(); 54 | 55 | Intent i = new Intent(); 56 | i.setClass(activity, errorIntentTargetClass); 57 | i.putExtra("stack_trace_string",stackTraceString); 58 | i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 59 | 60 | activity.startActivity(i); 61 | activity.finish(); 62 | 63 | if (executeOriginalUncaughtExceptionHandler && originalHandler != null) { 64 | originalHandler.uncaughtException(thread, throwable); 65 | } 66 | 67 | if (forceToQuit) { 68 | System.exit(0); 69 | } 70 | } 71 | } 72 | }); 73 | } 74 | 75 | public static void replaceErrorScreenActivityClass(Class errorScreenActivityClass){ 76 | errorIntentTargetClass = errorScreenActivityClass; 77 | } 78 | 79 | public static void setNativeExceptionHandler(NativeExceptionHandlerIfc nativeExceptionHandler) { 80 | ReactNativeExceptionHandlerModule.nativeExceptionHandler = nativeExceptionHandler; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /android/src/main/java/com/masteratul/exceptionhandler/ReactNativeExceptionHandlerPackage.java: -------------------------------------------------------------------------------- 1 | 2 | package com.masteratul.exceptionhandler; 3 | 4 | import java.util.Arrays; 5 | import java.util.Collections; 6 | import java.util.List; 7 | 8 | import com.facebook.react.ReactPackage; 9 | import com.facebook.react.bridge.NativeModule; 10 | import com.facebook.react.bridge.ReactApplicationContext; 11 | import com.facebook.react.uimanager.ViewManager; 12 | import com.facebook.react.bridge.JavaScriptModule; 13 | public class ReactNativeExceptionHandlerPackage implements ReactPackage { 14 | @Override 15 | public List createNativeModules(ReactApplicationContext reactContext) { 16 | return Arrays.asList(new ReactNativeExceptionHandlerModule(reactContext)); 17 | } 18 | 19 | public List> createJSModules() { 20 | return Collections.emptyList(); 21 | } 22 | 23 | @Override 24 | public List createViewManagers(ReactApplicationContext reactContext) { 25 | return Collections.emptyList(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /android/src/main/res/layout/default_error_screen.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 15 | 24 | 33 | 34 | 42 | 47 |