This episode breaks open debugging the JS-native bridge in React Native with Hermes: understanding the call flow across the native module boundary, seeing internal calls through the profiler, then handling crash reporting and native stack traces from production.

Episode 17 closed with GC tuning: understanding heap behavior, reducing pause time, and hunting memory leaks on low-memory devices. We're still in Phase 5 this time, but the focus shifts from memory to the boundary between languages: the JS-native bridge in React Native. If your app calls native modules, this is where the most mysterious errors are born — errors that don't show up as ordinary JavaScript stack traces.
Episode 18's roadmap: a map of the JS-native bridge with Hermes, how to debug calls crossing that boundary, seeing internal calls through the profiler, understanding data serialization at the boundary, then handling crash reporting and native stack traces.
React Native isn't a pure JavaScript app. Your JS code is executed by Hermes, while the UI, sensors, and system services live in native code. The two are connected by the bridge (the old architecture) or TurboModule + JSI (the New Architecture).
import { NativeModules } from "react-native";
const { FileScanner } = NativeModules;
async function scanWithLog(path) {
const started = Date.now();
try {
const result = await FileScanner.scanDirectory(path);
console.log(`[native-bridge] scan selesai dalam ${Date.now() - started}ms`);
return result;
} catch (err) {
console.warn("[native-bridge] scan gagal:", err.message);
throw err;
}
}Notice the pattern above: every time code crosses into native, there's a point where things can slow down or fail. Attaching logs and measurements right at the boundary helps debugging enormously.
Debugging the bridge starts with logs. In dev, run Metro in one terminal and the app in another:
npx react-native run-android
adb logcat -s ReactNativeJS:V ReactNative:VThe ReactNativeJS tag carries console.log from the JS code running in Hermes, while the ReactNative tag carries logs from the native side. If your native module call is silent without output, first check whether the JS log appears — if it doesn't, the problem is in Hermes or the bundle, not the native module. This is a very cheap isolation trick; npx react-native info also helps confirm the Hermes, React Native, and toolchain versions are consistent before blaming code.
Info
If you use the New Architecture, JSI makes calls synchronous and logs appear in a more natural order. In the old bridge architecture, the async queue can make log order look random — don't be fooled, order by timestamp, not by appearance.
Sometimes logs aren't enough — you need to see the full call stack, including where execution leaves JavaScript. Chrome DevTools / React Native DevTools connected to Hermes shows JS calls, but to see the native side, use a native profiler:
adb shell dumpsys gfxinfo <package> framestats provides frame rendering numbers you can compare with the JS profile.The key to understanding this: a JS function that looks "brief" can spend hundreds of milliseconds inside a native module underneath. A profiler that only sees JS will show a call stack ending at the _nativeCall point — that's a clue that the real time is being spent outside Hermes.
Every piece of data crossing the bridge must be serialized (via JSON in the old architecture). Large objects or unusual structures are often the culprits:
undefined values can turn into null or disappear after serialization.Date, Map, and Set don't serialize naturally — they turn into empty objects.try {
const stats = await NativeModules.DeviceStats.read();
console.log("DeviceStats keys:", Object.keys(stats ?? {}));
} catch (err) {
console.error("DeviceStats gagal:", err.code, err.userInfo ?? err.message);
}For frequently crossing calls, first test the data shape in dev with an Object.keys log like the one above before building assumptions. Even better: move heavy logic that only needs native access to the native side, so the bridge isn't bouncing back and forth.
JavaScript errors have JavaScript stack traces. But crashes in native modules — segmentation faults, memory errors on the JNI or ObjC side — don't produce JS stack traces. Hermes can still report, but for native stack traces we need special tools:
adb logcat -d | ndk-stack -sym build/app/intermediates/merged_native_libs/release/mergeReleaseNativeLibs/out/lib > crash-resolved.txtndk-stack maps addresses in the crash log to function names in native libraries. Make sure the symbols come from the same build that was released — a different build means different addresses and misleading results. You can also use addr2line with the right .so binary:
addr2line -f -C -e build/app/intermediates/merged_native_libs/release/mergeReleaseNativeLibs/out/lib/arm64-v8a/libreactnative.so 0x4a3f2cFor production, integrate a crash reporter like Sentry in Hermes + React Native mode. Sentry maps native stack traces to original functions and source maps to JS code:
import * as Sentry from "@sentry/react-native";
Sentry.init({
dsn: "https://dsn.kalian@sentry.io/1",
tracesSampleRate: 0.25,
environment: __DEV__ ? "development" : "production",
});Warning
Source maps and native symbols are a pair that must never be separated from the build version. Store both in a CI archive indexed by release version — without that, a production stack trace received a year from now can't be symbolized correctly.
An important habit: before releasing, test a crash in a crafted release build with adb shell am crash <package> and make sure the incoming report is readable — both on the JS side and the native side.
Debugging native integration is the most expensive skill in the React Native stack because its errors often have no JavaScript face. By mapping the JS-native bridge, attaching measurements at the boundary, using two-sided profilers, and setting up correct symbolication, you turn mysterious crashes into actionable reports.
The essentials to take home:
ReactNativeJS) from native logs (ReactNative) to isolate the problematic side.In episode 19, we enter Phase 6: Build Systems, CI & Release — defining CI pipelines for Hermes builds, validating bytecode and source maps, and enforcing deployment checks for Android and iOS. See you there!