Skip to content
This repository was archived by the owner on Mar 7, 2025. It is now read-only.

Commit d5e4d5c

Browse files
tomekzawfluiddot
authored andcommitted
Fix Android crash when worklet throws error (software-mansion#3558)
## Description This PR improves developer experience of error handling in worklets by showing a RedBox with full error message rather than crashing the whole app on Android. **Note:** Android with Fabric enabled works only if building with NDK 21 via `yarn react-native run-android [--active-arch-only]`, crashes when building with NDK 24 from Android Studio, see Shopify/flash-list#550 (comment) for details (this is not an issue with Reanimated) <table> <thead> <tr> <th rowspan="2">Scenario</th> <th colspan="2">Before</th> <th colspan="2">After</th> </tr> <tr> <th>Paper</th> <th>Fabric</th> <th>Paper</th> <th>Fabric</th> </thead> <tbody> <tr> <td>worklet</td> <td align="center">💥</td> <td align="center">💥</td> <td align="center">✅</td> <td align="center">✅</td> </tr> <tr> <td>nested worklet</td> <td align="center">💥</td> <td align="center">💥</td> <td align="center">✅</td> <td align="center">✅</td> </tr> <tr> <td>useAnimatedStyle</td> <td align="center">💥</td> <td align="center">💥</td> <td align="center">✅</td> <td align="center">✅</td> </tr> <tr> <td>useDerivedValue</td> <td align="center">💥</td> <td align="center">💥</td> <td align="center">✅</td> <td align="center">✅</td> </tr> <tr> <td>useFrameCallback</td> <td align="center">💥</td> <td align="center">💥</td> <td align="center">✅</td> <td align="center">✅</td> </tr> <tr> <td>GestureDetector</td> <td align="center">💥</td> <td align="center">💥</td> <td align="center">✅</td> <td align="center">✅</td> </tr> <tr> <td>useAnimatedGestureHandler</td> <td align="center">💥</td> <td align="center">💥</td> <td align="center">✅</td> <td align="center">✅</td> </tr> <tr> <td>useAnimatedScrollHandler</td> <td align="center">💥</td> <td align="center">💥</td> <td align="center">✅</td> <td align="center">✅</td> </tr> <tr> <td>useScrollViewOffset</td> <td align="center">💥</td> <td align="center">💥</td> <td align="center">✅</td> <td align="center">✅</td> </tr> </tbody> </table> ## Test code and steps to reproduce ### Building with NDK 21 on M1-based Mac https://github.com/software-mansion/react-native-reanimated/blob/48af341d51ca289dc007fdfd81d124ae0c267523/FabricExample/android/build.gradle#L11-L17 ```diff - ndkVersion = "24.0.8215888" + ndkVersion = "21.4.7075529" ``` `ERROR: Unknown host CPU architecture: arm64` ```console code ~/Library/Android/sdk/ndk/21.4.7075529/ndk-build ``` ```diff #!/bin/sh DIR="$(cd "$(dirname "$0")" && pwd)" -$DIR/build/ndk-build "$@" +arch -x86_64 $DIR/build/ndk-build "$@" ``` ## Checklist - [ ] Included code example that can be used to test this change - [ ] Updated TS types - [ ] Added TS types tests - [ ] Added unit / integration tests - [ ] Updated documentation - [ ] Ensured that CI passes
1 parent 70b1720 commit d5e4d5c

File tree

5 files changed

+252
-42
lines changed

5 files changed

+252
-42
lines changed

Common/cpp/SharedItems/ShareableValue.cpp

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -414,8 +414,6 @@ jsi::Value ShareableValue::toJSValue(jsi::Runtime &rt) {
414414
} else {
415415
res = funPtr->call(rt, args, count);
416416
}
417-
} catch (jsi::JSError &e) {
418-
throw e;
419417
} catch (std::exception &e) {
420418
std::string str = e.what();
421419
runtimeManager->errorHandler->setError(str);
Lines changed: 248 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,276 @@
1+
/* eslint-disable react-native/no-inline-styles */
12
/* global _WORKLET */
2-
import { Button, View, StyleSheet } from 'react-native';
3-
import {
3+
import Animated, {
44
runOnJS,
55
runOnUI,
6+
useAnimatedGestureHandler,
7+
useAnimatedRef,
8+
useAnimatedScrollHandler,
9+
useAnimatedStyle,
610
useDerivedValue,
11+
useFrameCallback,
12+
useScrollViewOffset,
713
useSharedValue,
814
} from 'react-native-reanimated';
15+
import { Button, Text, View } from 'react-native';
16+
import {
17+
Gesture,
18+
GestureDetector,
19+
PanGestureHandler,
20+
} from 'react-native-gesture-handler';
921

1022
import React from 'react';
1123

12-
export default function WorkletExample() {
13-
// runOnUI demo
14-
const someWorklet = (number: number) => {
24+
declare global {
25+
const _WORKLET: boolean;
26+
}
27+
28+
function RunOnUIDemo() {
29+
const someWorklet = (x: number) => {
1530
'worklet';
16-
console.log(_WORKLET, number); // _WORKLET should be true
31+
console.log(_WORKLET, x); // _WORKLET should be true
1732
};
1833

19-
const handlePress1 = () => {
34+
const handlePress = () => {
2035
runOnUI(someWorklet)(Math.random());
2136
};
2237

23-
// runOnJS demo
24-
const x = useSharedValue(0);
38+
return <Button onPress={handlePress} title="runOnUI demo" />;
39+
}
2540

26-
const someFunction = (number: number) => {
27-
console.log(_WORKLET, number); // _WORKLET should be false
41+
function RunOnUIRunOnJSDemo() {
42+
const someFunction = (x: number) => {
43+
console.log(_WORKLET, x); // _WORKLET should be false
44+
};
45+
46+
const someWorklet = (x: number) => {
47+
'worklet';
48+
console.log(_WORKLET, x); // _WORKLET should be true
49+
runOnJS(someFunction)(x);
50+
};
51+
52+
const handlePress = () => {
53+
runOnUI(someWorklet)(Math.random());
54+
};
55+
56+
return <Button onPress={handlePress} title="runOnUI + runOnJS demo" />;
57+
}
58+
59+
function UseDerivedValueRunOnJSDemo() {
60+
const sv = useSharedValue(0);
61+
62+
const someFunction = (x: number) => {
63+
console.log(_WORKLET, x); // _WORKLET should be false
2864
};
2965

3066
useDerivedValue(() => {
31-
runOnJS(someFunction)(x.value);
67+
console.log(_WORKLET, sv.value);
68+
runOnJS(someFunction)(sv.value);
69+
});
70+
71+
const handlePress = () => {
72+
sv.value = 1 + Math.random();
73+
};
74+
75+
return (
76+
<Button onPress={handlePress} title="useDerivedValue + runOnJS demo" />
77+
);
78+
}
79+
80+
function ThrowErrorDemo() {
81+
const handlePress = () => {
82+
throw new Error('Hello world from React Native JS!');
83+
};
84+
85+
return <Button onPress={handlePress} title="Throw error on JS" />;
86+
}
87+
88+
function ThrowErrorWorkletDemo() {
89+
const someWorklet = () => {
90+
'worklet';
91+
throw new Error('Hello world from worklet!');
92+
};
93+
94+
const handlePress = () => {
95+
runOnUI(someWorklet)();
96+
};
97+
98+
return <Button onPress={handlePress} title="Throw error from worklet" />;
99+
}
100+
101+
function ThrowErrorNestedWorkletDemo() {
102+
const innerWorklet = () => {
103+
'worklet';
104+
throw new Error('Hello world from nested worklet!');
105+
};
106+
107+
const outerWorklet = () => {
108+
'worklet';
109+
innerWorklet();
110+
};
111+
112+
const handlePress = () => {
113+
runOnUI(outerWorklet)();
114+
};
115+
116+
return (
117+
<Button onPress={handlePress} title="Throw error from nested worklet" />
118+
);
119+
}
120+
121+
function ThrowErrorFromUseAnimatedStyleDemo() {
122+
const sv = useSharedValue(0);
123+
124+
useAnimatedStyle(() => {
125+
if (!_WORKLET || sv.value === 0) {
126+
return {}; // prevent throwing error on first render or from JS context
127+
}
128+
throw new Error('Hello world from useAnimatedStyle!');
32129
});
33130

34-
const handlePress2 = () => {
35-
x.value = Math.random();
131+
const handlePress = () => {
132+
sv.value = 1 + Math.random();
36133
};
37134

38135
return (
39-
<View style={styles.container}>
40-
<Button onPress={handlePress1} title="runOnUI demo" />
41-
<Button onPress={handlePress2} title="runOnJS demo" />
136+
<Button onPress={handlePress} title="Throw error from useAnimatedStyle" />
137+
);
138+
}
139+
140+
function ThrowErrorFromUseDerivedValueDemo() {
141+
const sv = useSharedValue(0);
142+
143+
useDerivedValue(() => {
144+
if (!_WORKLET || sv.value === 0) {
145+
return {}; // prevent throwing error on first render or from JS context
146+
}
147+
throw new Error('Hello world from useDerivedValue!');
148+
}, [sv]);
149+
150+
const handlePress = () => {
151+
sv.value = 1 + Math.random();
152+
};
153+
154+
return (
155+
<Button onPress={handlePress} title="Throw error from useDerivedValue" />
156+
);
157+
}
158+
159+
function ThrowErrorFromUseFrameCallbackDemo() {
160+
const sv = useSharedValue(false);
161+
162+
useFrameCallback(() => {
163+
if (sv.value) {
164+
sv.value = false;
165+
throw new Error('Hello world from useFrameCallback!');
166+
}
167+
}, true);
168+
169+
const handlePress = () => {
170+
sv.value = true;
171+
};
172+
173+
return (
174+
<Button onPress={handlePress} title="Throw error from useFrameCallback" />
175+
);
176+
}
177+
178+
function ThrowErrorFromGestureDetectorDemo() {
179+
const gesture = Gesture.Pan().onChange(() => {
180+
throw Error('Hello world from GestureDetector callback!');
181+
});
182+
183+
return (
184+
<GestureDetector gesture={gesture}>
185+
<View style={{ width: 100, height: 100, backgroundColor: 'tomato' }}>
186+
<Text>GestureDetector</Text>
187+
</View>
188+
</GestureDetector>
189+
);
190+
}
191+
192+
function ThrowErrorFromUseAnimatedGestureHandlerDemo() {
193+
const gestureHandler = useAnimatedGestureHandler({
194+
onActive: () => {
195+
throw Error('Hello world from useAnimatedGestureHandler');
196+
},
197+
});
198+
199+
return (
200+
<PanGestureHandler onGestureEvent={gestureHandler}>
201+
<Animated.View
202+
style={{ width: 100, height: 100, backgroundColor: 'gold' }}>
203+
<Text>PanGestureHandler + useAnimatedGestureHandler</Text>
204+
</Animated.View>
205+
</PanGestureHandler>
206+
);
207+
}
208+
209+
function ThrowErrorFromUseAnimatedScrollHandlerDemo() {
210+
const scrollHandler = useAnimatedScrollHandler(() => {
211+
throw Error('Hello world from useAnimatedScrollHandler');
212+
});
213+
214+
return (
215+
<View style={{ height: 100 }}>
216+
<Animated.ScrollView scrollEventThrottle={16} onScroll={scrollHandler}>
217+
<View
218+
style={{
219+
width: 100,
220+
height: 500,
221+
backgroundColor: 'lime',
222+
}}>
223+
<Text>useAnimatedScrollHandler</Text>
224+
</View>
225+
</Animated.ScrollView>
226+
</View>
227+
);
228+
}
229+
230+
function ThrowErrorFromUseScrollViewOffsetDemo() {
231+
const aref = useAnimatedRef<Animated.ScrollView>();
232+
233+
const offset = useScrollViewOffset(aref);
234+
235+
useAnimatedStyle(() => {
236+
if (_WORKLET && offset.value > 0) {
237+
throw Error('Hello world from useScrollViewOffset');
238+
}
239+
return {};
240+
});
241+
242+
return (
243+
<View style={{ height: 100 }}>
244+
<Animated.ScrollView scrollEventThrottle={16} ref={aref}>
245+
<View
246+
style={{
247+
width: 100,
248+
height: 500,
249+
backgroundColor: 'cyan',
250+
}}>
251+
<Text>useScrollViewOffset + useAnimatedStyle</Text>
252+
</View>
253+
</Animated.ScrollView>
42254
</View>
43255
);
44256
}
45257

46-
const styles = StyleSheet.create({
47-
container: {
48-
flex: 1,
49-
alignItems: 'center',
50-
justifyContent: 'center',
51-
},
52-
});
258+
export default function WorkletExample() {
259+
return (
260+
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
261+
<RunOnUIDemo />
262+
<RunOnUIRunOnJSDemo />
263+
<UseDerivedValueRunOnJSDemo />
264+
<ThrowErrorDemo />
265+
<ThrowErrorWorkletDemo />
266+
<ThrowErrorNestedWorkletDemo />
267+
<ThrowErrorFromUseAnimatedStyleDemo />
268+
<ThrowErrorFromUseDerivedValueDemo />
269+
<ThrowErrorFromUseFrameCallbackDemo />
270+
<ThrowErrorFromGestureDetectorDemo />
271+
<ThrowErrorFromUseAnimatedGestureHandlerDemo />
272+
<ThrowErrorFromUseAnimatedScrollHandlerDemo />
273+
<ThrowErrorFromUseScrollViewOffsetDemo />
274+
</View>
275+
);
276+
}

android/src/main/cpp/AndroidErrorHandler.cpp

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#include "AndroidErrorHandler.h"
22
#include <fbjni/fbjni.h>
3+
#include <exception>
34
#include <string>
45
#include "Logger.h"
56

@@ -17,11 +18,9 @@ void AndroidErrorHandler::raiseSpec() {
1718
return;
1819
}
1920

20-
static const auto cls = javaClassStatic();
21-
static auto method = cls->getStaticMethod<void(std::string)>("raise");
22-
method(cls, error->message);
23-
21+
// mark error as handled before this method throws exception
2422
this->error->handled = true;
23+
throw std::runtime_error(this->error->message);
2524
}
2625

2726
std::shared_ptr<Scheduler> AndroidErrorHandler::getScheduler() {

android/src/main/cpp/AndroidErrorHandler.h

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,12 @@
1111

1212
namespace reanimated {
1313

14-
class AndroidErrorHandler : public JavaClass<AndroidErrorHandler>,
15-
public ErrorHandler {
14+
class AndroidErrorHandler : public ErrorHandler {
1615
std::shared_ptr<ErrorWrapper> error;
1716
std::shared_ptr<Scheduler> scheduler;
1817
void raiseSpec() override;
1918

2019
public:
21-
static auto constexpr kJavaDescriptor =
22-
"Lcom/swmansion/reanimated/AndroidErrorHandler;";
2320
explicit AndroidErrorHandler(std::shared_ptr<Scheduler> scheduler);
2421
std::shared_ptr<Scheduler> getScheduler() override;
2522
std::shared_ptr<ErrorWrapper> getError() override;

android/src/main/java/com/swmansion/reanimated/AndroidErrorHandler.java

Lines changed: 0 additions & 8 deletions
This file was deleted.

0 commit comments

Comments
 (0)