Debugging
React Native debugging toolchain — React DevTools, Flipper / new debugger, Hermes, native logs, crash reporting
React Native Debugging
React Native debugging spans three runtimes — JavaScript (Hermes or JSC), native iOS (Swift/Obj-C), native Android (Kotlin/Java) — and a bridge (or JSI) between them. A bug can live in any one; the cost of misdiagnosis is debugging the wrong layer for an afternoon.
The skill is recognizing which layer a symptom belongs to and reaching for the matching tool. For the language-agnostic debugging method (reproduce, shrink, hypothesize, verify), see Debugging under code-craft.
The Toolchain
| Tool | What it's for |
|---|---|
| React DevTools | Component tree, props/state inspection, profiler |
| Hermes Debugger / Chrome DevTools | JS breakpoints, source maps, network, memory |
New RN debugger (j in Metro) | Modern Hermes-aware debugger (RN 0.73+) |
| Reactotron | Custom inspector for state, network, async storage |
| Flipper (legacy) | RN's older debugging shell — deprecated in RN 0.77+ |
| Xcode | Native iOS code, MethodQueue, native crashes |
| Android Studio + Logcat | Native Android code, ANRs, native crashes |
| Sentry / Crashlytics / Bugsnag | Production crash collection, JS + native + symbolication |
The everyday workflow:
- React DevTools for component / state issues.
- Hermes debugger or Chrome DevTools for JS logic.
- Native debugger for anything that crosses the bridge.
React DevTools
The same tool used for web React, in standalone mode for RN:
npx react-devtools
# Then run the app; it connects automaticallyWhat it does best:
- Component tree — inspect props, state, hooks, context.
- Profiler — record renders and find the components doing too much work.
- Component highlight — flash the component on the device when selected in the tree.
Open it before opening anything else when the symptom is "wrong thing rendered" or "rendered too many times."
Hermes & the JS Debugger
Hermes is the default JS engine in RN 0.70+. It's faster and more memory-efficient than JSC, but debugger behavior differs slightly.
The modern debugger (RN 0.73+):
# In your Metro terminal, press 'j'This opens Chrome DevTools attached to the Hermes runtime — source maps work, breakpoints work, memory snapshots work.
For older RN versions, the legacy "Debug with Chrome" runs JS in Chrome and uses WebSocket to send commands to the device. Slower and less accurate (different JS engine semantics), but works.
Source maps
Without source maps, stack traces show minified or transformed code. The pattern:
- Hermes generates source maps automatically; ensure they're enabled in your
metro.config.jsand uploaded to crash reporters. - For Sentry, the
@sentry/react-nativepackage handles upload automatically when configured withauth-tokenandorg/project.
Hermes-specific gotchas
Function.prototype.toStringreturns less. Code that introspects function source breaks on Hermes.- No eval, no Function constructor. Code that builds functions at runtime breaks.
- Different error formatting. Stack traces and error messages differ slightly from V8.
If a library breaks only on Hermes, check its compatibility table or fall back to JSC for that build.
Network Debugging
Three options:
React Native Debugger
(deprecated but still in use) shows fetch/XHR traffic from JS.
Reactotron
Better than React Native Debugger for many cases; supports network, AsyncStorage, custom commands.
// reactotron.config.ts
import Reactotron from 'reactotron-react-native';
import { reactotronRedux } from 'reactotron-redux';
Reactotron
.configure({ name: 'MyApp' })
.useReactNative()
.use(reactotronRedux())
.connect();Then call Reactotron.log(), Reactotron.warn(), etc. from app code.
Charles Proxy / Proxyman
Native proxy that captures all network traffic, including native-initiated requests. The right tool when:
- A request originates from native code (not JS).
- TLS pinning is suspected.
- You want to rewrite responses on the fly to test edge cases.
Setting up requires installing the proxy's root CA on the device. On Android, certs installed at runtime are only trusted by debug builds (unless you ship a network_security_config.xml).
Native Debugging
iOS
Open ios/<App>.xcworkspace in Xcode. Run from Xcode (not from Metro) to:
- Hit breakpoints in Swift/Obj-C.
- See native logs in Xcode's console (RN's JS
console.logalso flows here when running attached). - Profile with Instruments.
- Inspect MainQueue vs background queue work — the most common iOS RN bug is calling UI APIs off the main thread.
For native crashes, the dSYM is required for symbolication. If a release build crashes and the stack is symbol-soup, the dSYM wasn't uploaded.
Android
Open android/ in Android Studio. Run from there (or use adb logcat for tail-only) to:
- Hit breakpoints in Kotlin/Java.
- Read Logcat (the firehose). Filter by app PID:
adb logcat -v color "*:S" ReactNativeJS:V ReactNative:V "com.yourapp:V" - Profile with the CPU and Memory profilers.
- Inspect ANRs (Application Not Responding) — usually long work on the main thread.
For native crashes, ProGuard mappings must be uploaded to your crash reporter. Without them, symbols are obfuscated.
The bridge / JSI
RN apps that crash inside react-native itself, with stacks that mention Bridge, JSIExecutor, or RCTBridge, are usually:
- A native module method being called with wrong types.
- A native module callback called twice (or not at all).
- A native module accessing a deallocated JS reference.
Diagnose by:
- Reproducing in debug mode.
- Logging at the entry and exit of every native module call.
- Checking that promises resolve or reject exactly once per call.
Common Failure Modes
"Network request failed"
The least helpful error in RN. It can mean:
- HTTP error.
- CORS / clear-text traffic blocked.
- Native TLS / cert pinning failure.
- DNS resolution failure on the device.
Diagnostic:
- iOS: App Transport Security blocks plain HTTP. Either use HTTPS or add an exception in
Info.plist. - Android: Clear-text traffic blocked by default since Android 9. Add
android:usesCleartextTraffic="true"to<application>(or per-domain config) for development builds. - Both: Run the request through Charles to see what actually went on the wire.
"Couldn't find component / Cannot read property of undefined"
Usually a render-during-render bug. The fix:
// Bad: state update during render
function Component({ data }) {
if (!data) {
setError('No data'); // throws — state update during render
}
return <View>...</View>;
}
// Good: state update in effect
function Component({ data }) {
useEffect(() => {
if (!data) setError('No data');
}, [data]);
return <View>...</View>;
}Blank screen on startup
Usually an unhandled promise rejection during init or a JS error before the first render. The clue is in adb logcat or Xcode console — RN prints the error but the UI never paints to show it.
For production, wire the global handler:
import { ErrorUtils } from 'react-native';
const original = ErrorUtils.getGlobalHandler();
ErrorUtils.setGlobalHandler((error, isFatal) => {
Sentry.captureException(error);
original(error, isFatal);
});"Element type is invalid: expected a string or a class/function"
A bad import:
import X from 'foo'whereXis a named export (should beimport { X } from 'foo').- Default export missing.
- Circular dependency that hasn't resolved yet.
The line number in the error gives the offending element. Most commonly the import is the culprit.
TouchableOpacity / Pressable doesn't respond
Usually one of:
- The view is covered by something with higher z-index (Android:
elevation). pointerEvents="none"is set on an ancestor.- The
onPressisundefined(typo or missing prop). - The view has
hitSlopissues — the touch area is smaller than the visible area.
Use the React DevTools to inspect props on the touchable; the answer is usually visible there.
Performance: dropped frames during list scroll
Almost always one of:
- Rendering too much per row (heavy components in a
FlatListitem). - Not using
keyExtractor(causes whole list to re-render). - Inline functions in props (causes children to re-render).
- Large images decoded at full size when displayed small.
Fix:
- Memoize the row component (
React.memo). - Move handlers out of render with
useCallback. - Use
FlatList'sgetItemLayoutif rows are uniform height. - Use the
imagelibrary's resize / cache config.
Yellow / red box warnings in production
LogBox suppresses by default in release builds, but the underlying problem (deprecated API, key warnings, etc.) is still real. Fix the cause, don't suppress with LogBox.ignoreLogs.
Crash Reporting
Three layers must all be wired up for cross-platform production debugging:
- JS errors — RN's global handler (above) or Sentry's automatic wrap.
- Native iOS crashes — dSYMs uploaded to the reporter.
- Native Android crashes — ProGuard mappings uploaded.
A common pattern with Sentry:
import * as Sentry from '@sentry/react-native';
Sentry.init({
dsn: 'https://...',
tracesSampleRate: 0.1,
environment: __DEV__ ? 'development' : 'production',
});The @sentry/react-native Metro plugin uploads source maps and native symbols automatically when configured in the build pipeline. Without that step, production stacks are unreadable.
Performance Profiling
For JS performance:
- React DevTools profiler shows render timings.
- Hermes has
--profileflags that produce a sampling profile. - Reactotron's "Overlay" feature shows component render counts in real time.
For native performance:
- Xcode Instruments (Time Profiler, Allocations).
- Android Studio CPU Profiler.
flashlightfor end-to-end mobile performance.
Performance numbers from __DEV__ builds are not representative. Always measure in release.
Pre-Commit Checklist
For changes that touched debugging-relevant code:
- No leftover
console.login production code paths (or LogBox is configured to silence them). - No leftover
debugger;statements. - Effects cancel async work in their cleanup function.
- Async state updates guard with
isMountedor use AbortController. - Native module errors propagate to JS via reject/error callbacks.
- Crash reporter has source maps, dSYMs, and ProGuard mappings uploaded by CI.
- Performance numbers, if claimed, were captured in release builds.