diff --git a/React/Base/RCTBridge.m b/React/Base/RCTBridge.m index c48afa565..158ca38e6 100644 --- a/React/Base/RCTBridge.m +++ b/React/Base/RCTBridge.m @@ -1038,6 +1038,9 @@ static id _latestJSExecutor; } @catch (NSException *exception) { RCTLogError(@"Exception thrown while invoking %@ on target %@ with params %@: %@", method.JSMethodName, module, params, exception); + if ([exception.name rangeOfString:@"Unhandled JS Exception"].location != NSNotFound) { + @throw; + } } }); diff --git a/React/Modules/RCTExceptionsManager.h b/React/Modules/RCTExceptionsManager.h index ddbb44869..25e0fbefc 100644 --- a/React/Modules/RCTExceptionsManager.h +++ b/React/Modules/RCTExceptionsManager.h @@ -21,4 +21,6 @@ - (instancetype)initWithDelegate:(id)delegate NS_DESIGNATED_INITIALIZER; +@property (nonatomic, assign) NSUInteger maxReloadAttempts; + @end diff --git a/React/Modules/RCTExceptionsManager.m b/React/Modules/RCTExceptionsManager.m index 1ce5c7383..5be80133b 100644 --- a/React/Modules/RCTExceptionsManager.m +++ b/React/Modules/RCTExceptionsManager.m @@ -9,19 +9,27 @@ #import "RCTExceptionsManager.h" +#import "RCTLog.h" #import "RCTRedBox.h" +#import "RCTRootView.h" @implementation RCTExceptionsManager { __weak id _delegate; + NSUInteger _reloadRetries; } +#ifndef DEBUG +static NSUInteger RCTReloadRetries = 0; +#endif + RCT_EXPORT_MODULE() - (instancetype)initWithDelegate:(id)delegate { if ((self = [super init])) { _delegate = delegate; + _maxReloadAttempts = 0; } return self; } @@ -36,9 +44,40 @@ RCT_EXPORT_METHOD(reportUnhandledException:(NSString *)message { if (_delegate) { [_delegate unhandledJSExceptionWithMessage:message stack:stack]; - } else { - [[RCTRedBox sharedInstance] showErrorMessage:message withStack:stack]; + return; } + +#ifdef DEBUG + [[RCTRedBox sharedInstance] showErrorMessage:message withStack:stack]; +#else + if (RCTReloadRetries < _maxReloadAttempts) { + RCTReloadRetries++; + dispatch_async(dispatch_get_main_queue(), ^{ + [[NSNotificationCenter defaultCenter] postNotificationName:RCTReloadNotification object:nil]; + }); + } else { + NSError *error; + const NSUInteger MAX_SANITIZED_LENGTH = 75; + // Filter out numbers so the same base errors are mapped to the same categories independent of incorrect values. + NSString *pattern = @"[+-]?\\d+[,.]?\\d*"; + NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:pattern options:0 error:&error]; + RCTAssert(error == nil, @"Bad regex pattern: %@", pattern); + NSString *sanitizedMessage = [regex stringByReplacingMatchesInString:message + options:0 + range:NSMakeRange(0, message.length) + withTemplate:@""]; + if (sanitizedMessage.length > MAX_SANITIZED_LENGTH) { + sanitizedMessage = [[sanitizedMessage substringToIndex:MAX_SANITIZED_LENGTH] stringByAppendingString:@"..."]; + } + NSMutableString *prettyStack = [@"\n" mutableCopy]; + for (NSDictionary *frame in stack) { + [prettyStack appendFormat:@"%@@%@:%@\n", frame[@"methodName"], frame[@"lineNumber"], frame[@"column"]]; + } + + NSString *name = [@"Unhandled JS Exception: " stringByAppendingString:sanitizedMessage]; + [NSException raise:name format:@"Message: %@, stack: %@", message, prettyStack]; + } +#endif } RCT_EXPORT_METHOD(updateExceptionMessage:(NSString *)message