Report JSC errors as JS exceptions

Summary:When JSC throws an error on startup (e.g. a SyntaxError) or when invoking a method that is not caught by RCTExceptionsManager, we previously just reported is a native error, with a (useless) native stack trace in the redbox. This changes that behaviour to report a JS stacktrace.

The same issue was previously reported here: https://github.com/facebook/react-native/pull/5677

Reviewed By: majak

Differential Revision: D3037387

fb-gh-sync-id: 06f8333e0eb50dcef0b26284754262301b8a5f08
fbshipit-source-id: 06f8333e0eb50dcef0b26284754262301b8a5f08
This commit is contained in:
Pieter De Baets 2016-04-20 09:12:18 -07:00 committed by Facebook Github Bot 3
parent ed930b4710
commit 5cdfe0f4b1
3 changed files with 87 additions and 35 deletions

View File

@ -23,6 +23,7 @@
#import "RCTProfile.h"
#import "RCTSourceCode.h"
#import "RCTUtils.h"
#import "RCTRedBox.h"
#define RCTAssertJSThread() \
RCTAssert(![NSStringFromClass([_javaScriptExecutor class]) isEqualToString:@"RCTJSCExecutor"] || \
@ -506,6 +507,10 @@ RCT_EXTERN NSArray<Class> *RCTGetModuleClasses(void);
postNotificationName:RCTJavaScriptDidFailToLoadNotification
object:_parentBridge userInfo:@{@"bridge": self, @"error": error}];
if ([error userInfo][RCTJSStackTraceKey]) {
[self.redBox showErrorMessage:[error localizedDescription]
withStack:[error userInfo][RCTJSStackTraceKey]];
}
RCTFatal(error);
}
@ -776,21 +781,13 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithBundleURL:(__unused NSURL *)bundleUR
{
RCTAssertJSThread();
RCTJavaScriptCallback processResponse = ^(id json, NSError *error) {
if (error) {
RCTFatal(error);
}
if (!_valid) {
return;
}
[self handleBuffer:json batchEnded:YES];
};
__weak typeof(self) weakSelf = self;
[_javaScriptExecutor callFunctionOnModule:module
method:method
arguments:args
callback:processResponse];
callback:^(id json, NSError *error) {
[weakSelf _processResponse:json error:error];
}];
}
- (void)_actuallyInvokeCallback:(NSNumber *)cbID
@ -798,20 +795,28 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithBundleURL:(__unused NSURL *)bundleUR
{
RCTAssertJSThread();
RCTJavaScriptCallback processResponse = ^(id json, NSError *error) {
if (error) {
RCTFatal(error);
}
if (!_valid) {
return;
}
[self handleBuffer:json batchEnded:YES];
};
__weak typeof(self) weakSelf = self;
[_javaScriptExecutor invokeCallbackID:cbID
arguments:args
callback:processResponse];
callback:^(id json, NSError *error) {
[weakSelf _processResponse:json error:error];
}];
}
- (void)_processResponse:(id)json error:(NSError *)error
{
if (error) {
if ([error userInfo][RCTJSStackTraceKey]) {
[self.redBox showErrorMessage:[error localizedDescription]
withStack:[error userInfo][RCTJSStackTraceKey]];
}
RCTFatal(error);
}
if (!_valid) {
return;
}
[self handleBuffer:json batchEnded:YES];
}
#pragma mark - Payload Processing

View File

@ -140,9 +140,55 @@ static NSString *RCTJSValueToJSONString(JSContextRef context, JSValueRef value,
static NSError *RCTNSErrorFromJSError(JSContextRef context, JSValueRef jsError)
{
NSString *errorMessage = jsError ? RCTJSValueToNSString(context, jsError, NULL) : @"Unknown JS error";
NSString *details = jsError ? RCTJSValueToJSONString(context, jsError, NULL, 2) : @"No details";
return [NSError errorWithDomain:@"JS" code:1 userInfo:@{NSLocalizedDescriptionKey: errorMessage, NSLocalizedFailureReasonErrorKey: details}];
NSMutableDictionary *errorInfo = [NSMutableDictionary new];
NSString *description = jsError ? RCTJSValueToNSString(context, jsError, NULL) : @"Unknown JS error";
errorInfo[NSLocalizedDescriptionKey] = [@"Unhandled JS Exception: " stringByAppendingString:description];
NSString *details = jsError ? RCTJSValueToJSONString(context, jsError, NULL, 0) : nil;
if (details) {
errorInfo[NSLocalizedFailureReasonErrorKey] = details;
// Format stack as used in RCTFormatError
id json = RCTJSONParse(details, NULL);
if ([json isKindOfClass:[NSDictionary class]]) {
if (json[@"stack"]) {
NSError *regexError;
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"^([^@]+)@(.*):(\\d+):(\\d+)$" options:0 error:&regexError];
if (regexError) {
RCTLogError(@"Failed to build regex: %@", [regexError localizedDescription]);
}
NSMutableArray *stackTrace = [NSMutableArray array];
for (NSString *stackLine in [json[@"stack"] componentsSeparatedByString:@"\n"]) {
NSTextCheckingResult *result = [regex firstMatchInString:stackLine options:0 range:NSMakeRange(0, stackLine.length)];
if (result) {
[stackTrace addObject:@{
@"methodName": [stackLine substringWithRange:[result rangeAtIndex:1]],
@"file": [stackLine substringWithRange:[result rangeAtIndex:2]],
@"lineNumber": [stackLine substringWithRange:[result rangeAtIndex:3]],
@"column": [stackLine substringWithRange:[result rangeAtIndex:4]]
}];
}
}
if ([stackTrace count]) {
errorInfo[RCTJSStackTraceKey] = stackTrace;
}
}
// Fall back to just logging the line number
if (!errorInfo[RCTJSStackTraceKey] && json[@"line"]) {
errorInfo[RCTJSStackTraceKey] = @[@{
@"methodName": @"",
@"file": RCTNullIfNil(json[@"sourceURL"]),
@"lineNumber": RCTNullIfNil(json[@"line"]),
@"column": @0,
}];
}
}
}
return [NSError errorWithDomain:RCTErrorDomain code:1 userInfo:errorInfo];
}
#if RCT_DEV
@ -630,7 +676,7 @@ static void RCTInstallJSCProfiler(RCTBridge *bridge, JSContextRef context)
RCTLogError(@"%@", errorDesc);
if (onComplete) {
NSError *error = [NSError errorWithDomain:@"JS" code:2 userInfo:@{NSLocalizedDescriptionKey: errorDesc}];
NSError *error = [NSError errorWithDomain:RCTErrorDomain code:2 userInfo:@{NSLocalizedDescriptionKey: errorDesc}];
onComplete(error);
}
return;

View File

@ -103,9 +103,10 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder)
[[[NSURLSession sharedSession] dataTaskWithRequest:request] resume];
}
- (void)showErrorMessage:(NSString *)message withStack:(NSArray<NSDictionary *> *)stack showIfHidden:(BOOL)shouldShow
- (void)showErrorMessage:(NSString *)message withStack:(NSArray<NSDictionary *> *)stack isUpdate:(BOOL)isUpdate
{
if ((self.hidden && shouldShow) || (!self.hidden && [_lastErrorMessage isEqualToString:message])) {
// Show if this is a new message, or if we're updating the previous message
if ((self.hidden && !isUpdate) || (!self.hidden && isUpdate && [_lastErrorMessage isEqualToString:message])) {
_lastStackTrace = stack;
// message is displayed using UILabel, which is unable to render text of
// unlimited length, so we truncate it
@ -277,7 +278,7 @@ RCT_EXPORT_MODULE()
- (void)showErrorMessage:(NSString *)message
{
[self showErrorMessage:message withStack:nil showIfHidden:YES];
[self showErrorMessage:message withStack:nil isUpdate:NO];
}
- (void)showErrorMessage:(NSString *)message withDetails:(NSString *)details
@ -291,21 +292,21 @@ RCT_EXPORT_MODULE()
- (void)showErrorMessage:(NSString *)message withStack:(NSArray<NSDictionary *> *)stack
{
[self showErrorMessage:message withStack:stack showIfHidden:YES];
[self showErrorMessage:message withStack:stack isUpdate:NO];
}
- (void)updateErrorMessage:(NSString *)message withStack:(NSArray<NSDictionary *> *)stack
{
[self showErrorMessage:message withStack:stack showIfHidden:NO];
[self showErrorMessage:message withStack:stack isUpdate:YES];
}
- (void)showErrorMessage:(NSString *)message withStack:(NSArray<NSDictionary *> *)stack showIfHidden:(BOOL)shouldShow
- (void)showErrorMessage:(NSString *)message withStack:(NSArray<NSDictionary *> *)stack isUpdate:(BOOL)isUpdate
{
dispatch_async(dispatch_get_main_queue(), ^{
if (!_window) {
_window = [[RCTRedBoxWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
}
[_window showErrorMessage:message withStack:stack showIfHidden:shouldShow];
[_window showErrorMessage:message withStack:stack isUpdate:isUpdate];
});
}