Skip to content

Commit 31b5b0a

Browse files
javachefacebook-github-bot-5
authored andcommitted
Create RCTFatal for reporting fatal React events
Summary: public Add RCTFatal for reporting fatal runtime conditions. This centralizes failure handling to one function and allows you to customize how they should be handled. RCTFatal will be logged to the console and as a redbox and will also be triggered by fatal exceptions coming from RCTExceptionsManager. Note that there is no RCTLogFatal, since just logging the fatal condition does not allow us to handle it consistently. Reviewed By: nicklockwood Differential Revision: D2615490 fb-gh-sync-id: 7d8e134419e10a8fb549297054ad955db3f6bee0
1 parent d8dd330 commit 31b5b0a

File tree

10 files changed

+139
-123
lines changed

10 files changed

+139
-123
lines changed

React/Base/RCTAssert.h

Lines changed: 42 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -11,25 +11,10 @@
1111

1212
#import "RCTDefines.h"
1313

14-
/**
15-
* The default error domain to be used for React errors.
16-
*/
17-
RCT_EXTERN NSString *const RCTErrorDomain;
18-
19-
/**
20-
* A block signature to be used for custom assertion handling.
21-
*/
22-
typedef void (^RCTAssertFunction)(
23-
NSString *condition,
24-
NSString *fileName,
25-
NSNumber *lineNumber,
26-
NSString *function,
27-
NSString *message
28-
);
29-
3014
/**
3115
* This is the main assert macro that you should use. Asserts should be compiled out
32-
* in production builds
16+
* in production builds. You can customize the assert behaviour by setting a custom
17+
* assert handler through `RCTSetAssertFunction`.
3318
*/
3419
#ifndef NS_BLOCK_ASSERTIONS
3520
#define RCTAssert(condition, ...) do { \
@@ -48,21 +33,50 @@ RCT_EXTERN void _RCTAssertFormat(
4833
const char *, const char *, int, const char *, NSString *, ...
4934
) NS_FORMAT_FUNCTION(5,6);
5035

36+
/**
37+
* Report a fatal condition when executing. These calls will _NOT_ be compiled out
38+
* in production, and crash the app by default. You can customize the fatal behaviour
39+
* by setting a custom fatal handler through `RCTSetFatalHandler`.
40+
*/
41+
RCT_EXTERN void RCTFatal(NSError *error);
42+
43+
/**
44+
* The default error domain to be used for React errors.
45+
*/
46+
RCT_EXTERN NSString *const RCTErrorDomain;
47+
48+
/**
49+
* JS Stack trace provided as part of an NSError's userInfo
50+
*/
51+
RCT_EXTERN NSString *const RCTJSStackTraceKey;
52+
53+
/**
54+
* A block signature to be used for custom assertion handling.
55+
*/
56+
typedef void (^RCTAssertFunction)(
57+
NSString *condition,
58+
NSString *fileName,
59+
NSNumber *lineNumber,
60+
NSString *function,
61+
NSString *message
62+
);
63+
64+
typedef void (^RCTFatalHandler)(NSError *error);
65+
5166
/**
5267
* Convenience macro for asserting that a parameter is non-nil/non-zero.
5368
*/
54-
#define RCTAssertParam(name) RCTAssert(name, \
55-
@"'%s' is a required parameter", #name)
69+
#define RCTAssertParam(name) RCTAssert(name, @"'%s' is a required parameter", #name)
5670

5771
/**
5872
* Convenience macro for asserting that we're running on main thread.
5973
*/
6074
#define RCTAssertMainThread() RCTAssert([NSThread isMainThread], \
61-
@"This function must be called on the main thread")
75+
@"This function must be called on the main thread")
6276

6377
/**
6478
* These methods get and set the current assert function called by the RCTAssert
65-
* macros. You can use these to replace the standard behavior with custom log
79+
* macros. You can use these to replace the standard behavior with custom assert
6680
* functionality.
6781
*/
6882
RCT_EXTERN void RCTSetAssertFunction(RCTAssertFunction assertFunction);
@@ -82,6 +96,12 @@ RCT_EXTERN void RCTAddAssertFunction(RCTAssertFunction assertFunction);
8296
*/
8397
RCT_EXTERN void RCTPerformBlockWithAssertFunction(void (^block)(void), RCTAssertFunction assertFunction);
8498

99+
/**
100+
These methods get and set the current fatal handler called by the RCTFatal method.
101+
*/
102+
RCT_EXTERN void RCTSetFatalHandler(RCTFatalHandler fatalHandler);
103+
RCT_EXTERN RCTFatalHandler RCTGetFatalHandler(void);
104+
85105
/**
86106
* Get the current thread's name (or the current queue, if in debug mode)
87107
*/
@@ -106,6 +126,6 @@ _Pragma("clang diagnostic pop")
106126

107127
#else
108128

109-
#define RCTAssertThread(thread, format...)
129+
#define RCTAssertThread(thread, format...) do { } while (0)
110130

111131
#endif

React/Base/RCTAssert.m

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,15 @@
88
*/
99

1010
#import "RCTAssert.h"
11+
#import "RCTLog.h"
1112

1213
NSString *const RCTErrorDomain = @"RCTErrorDomain";
14+
NSString *const RCTJSStackTraceKey = @"RCTJSStackTraceKey";
1315

1416
static NSString *const RCTAssertFunctionStack = @"RCTAssertFunctionStack";
1517

1618
RCTAssertFunction RCTCurrentAssertFunction = nil;
19+
RCTFatalHandler RCTCurrentFatalHandler = nil;
1720

1821
NSException *_RCTNotImplementedException(SEL, Class);
1922
NSException *_RCTNotImplementedException(SEL cmd, Class cls)
@@ -112,3 +115,44 @@ void _RCTAssertFormat(
112115
assertFunction(@(condition), @(fileName), @(lineNumber), @(function), message);
113116
}
114117
}
118+
119+
void RCTFatal(NSError *error)
120+
{
121+
_RCTLogInternal(RCTLogLevelFatal, NULL, 0, @"%@", [error localizedDescription]);
122+
123+
RCTFatalHandler fatalHandler = RCTGetFatalHandler();
124+
if (fatalHandler) {
125+
fatalHandler(error);
126+
} else {
127+
const NSUInteger maxMessageLength = 75;
128+
NSString *message = [error localizedDescription];
129+
if (message.length > maxMessageLength) {
130+
message = [[message substringToIndex:maxMessageLength] stringByAppendingString:@"..."];
131+
}
132+
133+
NSMutableString *prettyStack = [NSMutableString stringWithString:@"\n"];
134+
if ([error.userInfo[RCTJSStackTraceKey] isKindOfClass:[NSArray class]]) {
135+
for (NSDictionary *frame in error.userInfo[RCTJSStackTraceKey]) {
136+
[prettyStack appendFormat:@"%@@%@:%@\n", frame[@"methodName"], frame[@"lineNumber"], frame[@"column"]];
137+
}
138+
}
139+
140+
#if DEBUG
141+
@try {
142+
#endif
143+
[NSException raise:@"RCTFatalException" format:@"%@", message];
144+
#if DEBUG
145+
} @catch (NSException *e) {}
146+
#endif
147+
}
148+
}
149+
150+
void RCTSetFatalHandler(RCTFatalHandler fatalhandler)
151+
{
152+
RCTCurrentFatalHandler = fatalhandler;
153+
}
154+
155+
RCTFatalHandler RCTGetFatalHandler(void)
156+
{
157+
return RCTCurrentFatalHandler;
158+
}

React/Base/RCTBatchedBridge.m

Lines changed: 16 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
#import "RCTModuleMap.h"
2222
#import "RCTPerformanceLogger.h"
2323
#import "RCTProfile.h"
24-
#import "RCTRedBox.h"
2524
#import "RCTSourceCode.h"
2625
#import "RCTSparseArray.h"
2726
#import "RCTUtils.h"
@@ -416,18 +415,10 @@ - (void)stopLoadingWithError:(NSError *)error
416415

417416
_loading = NO;
418417

419-
NSArray<NSDictionary *> *stack = error.userInfo[@"stack"];
420-
if (stack) {
421-
[self.redBox showErrorMessage:error.localizedDescription withStack:stack];
422-
} else {
423-
[self.redBox showError:error];
424-
}
425-
RCTLogError(@"Error while loading: %@", error.localizedDescription);
426-
427-
NSDictionary *userInfo = @{@"bridge": self, @"error": error};
428418
[[NSNotificationCenter defaultCenter] postNotificationName:RCTJavaScriptDidFailToLoadNotification
429419
object:_parentBridge
430-
userInfo:userInfo];
420+
userInfo:@{@"bridge": self, @"error": error}];
421+
RCTFatal(error);
431422
}
432423

433424
RCT_NOT_IMPLEMENTED(- (instancetype)initWithBundleURL:(__unused NSURL *)bundleURL
@@ -661,7 +652,7 @@ - (void)_actuallyInvokeAndProcessModule:(NSString *)module
661652

662653
RCTJavaScriptCallback processResponse = ^(id json, NSError *error) {
663654
if (error) {
664-
[self.redBox showError:error];
655+
RCTFatal(error);
665656
}
666657

667658
if (!self.isValid) {
@@ -810,17 +801,23 @@ - (BOOL)_handleRequestNumber:(NSUInteger)i
810801
[method invokeWithBridge:self module:moduleData.instance arguments:params];
811802
}
812803
@catch (NSException *exception) {
813-
RCTLogError(@"Exception thrown while invoking %@ on target %@ with params %@: %@", method.JSMethodName, moduleData.name, params, exception);
814-
if (!RCT_DEBUG && [exception.name rangeOfString:@"Unhandled JS Exception"].location != NSNotFound) {
804+
// Pass on JS exceptions
805+
if ([exception.name rangeOfString:@"Unhandled JS Exception"].location == 0) {
815806
@throw exception;
816807
}
817-
}
818808

819-
NSMutableDictionary *args = [method.profileArgs mutableCopy];
820-
[args setValue:method.JSMethodName forKey:@"method"];
821-
[args setValue:RCTJSONStringify(RCTNullIfNil(params), NULL) forKey:@"args"];
809+
NSString *message = [NSString stringWithFormat:
810+
@"Exception thrown while invoking %@ on target %@ with params %@: %@",
811+
method.JSMethodName, moduleData.name, params, exception];
812+
RCTFatal(RCTErrorWithMessage(message));
813+
}
822814

823-
RCTProfileEndEvent(0, @"objc_call", args);
815+
if (RCTProfileIsProfiling()) {
816+
NSMutableDictionary *args = [method.profileArgs mutableCopy];
817+
args[@"method"] = method.JSMethodName;
818+
args[@"args"] = RCTJSONStringify(RCTNullIfNil(params), NULL);
819+
RCTProfileEndEvent(0, @"objc_call", args);
820+
}
824821

825822
return YES;
826823
}

React/Base/RCTBridge.m

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -284,8 +284,9 @@ - (NSDictionary *)modules
284284
#define RCT_INNER_BRIDGE_ONLY(...) \
285285
- (void)__VA_ARGS__ \
286286
{ \
287-
RCTLogMustFix(@"Called method \"%@\" on top level bridge. This method should \
288-
only be called from bridge instance in a bridge module", @(__func__)); \
287+
NSString *errorMessage = [NSString stringWithFormat:@"Called method \"%@\" on top level bridge. \
288+
This method should oly be called from bridge instance in a bridge module", @(__func__)]; \
289+
RCTFatal(RCTErrorWithMessage(errorMessage)); \
289290
}
290291

291292
- (void)enqueueJSCall:(NSString *)moduleDotMethod args:(NSArray *)args

React/Base/RCTLog.h

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,9 @@
1717
#endif
1818

1919
/**
20-
* Thresholds for logs to raise an assertion, or display redbox, respectively.
21-
* You can override these values when debugging in order to tweak the default
22-
* logging behavior.
20+
* Thresholds for logs to display a redbox. You can override these values when debugging
21+
* in order to tweak the default logging behavior.
2322
*/
24-
#ifndef RCTLOG_FATAL_LEVEL
25-
#define RCTLOG_FATAL_LEVEL RCTLogLevelMustFix
26-
#endif
27-
2823
#ifndef RCTLOG_REDBOX_LEVEL
2924
#define RCTLOG_REDBOX_LEVEL RCTLogLevelError
3025
#endif
@@ -37,7 +32,6 @@
3732
#define RCTLogInfo(...) _RCTLog(RCTLogLevelInfo, __VA_ARGS__)
3833
#define RCTLogWarn(...) _RCTLog(RCTLogLevelWarning, __VA_ARGS__)
3934
#define RCTLogError(...) _RCTLog(RCTLogLevelError, __VA_ARGS__)
40-
#define RCTLogMustFix(...) _RCTLog(RCTLogLevelMustFix, __VA_ARGS__)
4135

4236
/**
4337
* An enum representing the severity of the log message.
@@ -46,7 +40,7 @@ typedef NS_ENUM(NSInteger, RCTLogLevel) {
4640
RCTLogLevelInfo = 1,
4741
RCTLogLevelWarning = 2,
4842
RCTLogLevelError = 3,
49-
RCTLogLevelMustFix = 4
43+
RCTLogLevelFatal = 4
5044
};
5145

5246
/**
@@ -119,9 +113,7 @@ RCT_EXTERN void RCTPerformBlockWithLogPrefix(void (^block)(void), NSString *pref
119113
* Private logging function - ignore this.
120114
*/
121115
#if RCTLOG_ENABLED
122-
#define _RCTLog(lvl, ...) do { \
123-
if (lvl >= RCTLOG_FATAL_LEVEL) { RCTAssert(NO, __VA_ARGS__); } \
124-
_RCTLogInternal(lvl, __FILE__, __LINE__, __VA_ARGS__); } while (0)
116+
#define _RCTLog(lvl, ...) _RCTLogInternal(lvl, __FILE__, __LINE__, __VA_ARGS__);
125117
#else
126118
#define _RCTLog(lvl, ...) do { } while (0)
127119
#endif

React/Base/RCTLog.m

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ void RCTSetLogThreshold(RCTLogLevel threshold) {
6363
fprintf(stderr, "%s\n", log.UTF8String);
6464
fflush(stderr);
6565

66-
int aslLevel = ASL_LEVEL_ERR;
66+
int aslLevel;
6767
switch(level) {
6868
case RCTLogLevelInfo:
6969
aslLevel = ASL_LEVEL_NOTICE;
@@ -74,11 +74,9 @@ void RCTSetLogThreshold(RCTLogLevel threshold) {
7474
case RCTLogLevelError:
7575
aslLevel = ASL_LEVEL_ERR;
7676
break;
77-
case RCTLogLevelMustFix:
78-
aslLevel = ASL_LEVEL_EMERG;
77+
case RCTLogLevelFatal:
78+
aslLevel = ASL_LEVEL_CRIT;
7979
break;
80-
default:
81-
aslLevel = ASL_LEVEL_DEBUG;
8280
}
8381
asl_log(NULL, NULL, aslLevel, "%s", message.UTF8String);
8482
};

React/Base/RCTUtils.m

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -400,8 +400,7 @@ BOOL RCTImageHasAlpha(CGImageRef image)
400400
NSError *RCTErrorWithMessage(NSString *message)
401401
{
402402
NSDictionary *errorInfo = @{NSLocalizedDescriptionKey: message};
403-
NSError *error = [[NSError alloc] initWithDomain:RCTErrorDomain code:0 userInfo:errorInfo];
404-
return error;
403+
return [[NSError alloc] initWithDomain:RCTErrorDomain code:0 userInfo:errorInfo];
405404
}
406405

407406
id RCTNullIfNil(id value)

React/Modules/RCTExceptionsManager.h

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,11 @@
1313

1414
@protocol RCTExceptionsManagerDelegate <NSObject>
1515

16-
// NOTE: Remove these three methods and the @optional directive after updating the codebase to use only the three below
17-
@optional
18-
19-
- (void)handleSoftJSExceptionWithMessage:(NSString *)message stack:(NSArray<NSDictionary *> *)stack;
20-
- (void)handleFatalJSExceptionWithMessage:(NSString *)message stack:(NSArray<NSDictionary *> *)stack;
21-
- (void)updateJSExceptionWithMessage:(NSString *)message stack:(NSArray<NSDictionary *> *)stack;
16+
- (void)handleSoftJSExceptionWithMessage:(NSString *)message stack:(NSArray *)stack exceptionId:(NSNumber *)exceptionId;
17+
- (void)handleFatalJSExceptionWithMessage:(NSString *)message stack:(NSArray *)stack exceptionId:(NSNumber *)exceptionId;
2218

23-
- (void)handleSoftJSExceptionWithMessage:(NSString *)message stack:(NSArray<NSDictionary *> *)stack exceptionId:(NSNumber *)exceptionId;
24-
- (void)handleFatalJSExceptionWithMessage:(NSString *)message stack:(NSArray<NSDictionary *> *)stack exceptionId:(NSNumber *)exceptionId;
25-
- (void)updateJSExceptionWithMessage:(NSString *)message stack:(NSArray<NSDictionary *> *)stack exceptionId:(NSNumber *)exceptionId;
19+
@optional
20+
- (void)updateJSExceptionWithMessage:(NSString *)message stack:(NSArray *)stack exceptionId:(NSNumber *)exceptionId;
2621

2722
@end
2823

0 commit comments

Comments
 (0)