Steven's Knowledge

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

ToolWhat it's for
React DevToolsComponent tree, props/state inspection, profiler
Hermes Debugger / Chrome DevToolsJS breakpoints, source maps, network, memory
New RN debugger (j in Metro)Modern Hermes-aware debugger (RN 0.73+)
ReactotronCustom inspector for state, network, async storage
Flipper (legacy)RN's older debugging shell — deprecated in RN 0.77+
XcodeNative iOS code, MethodQueue, native crashes
Android Studio + LogcatNative Android code, ANRs, native crashes
Sentry / Crashlytics / BugsnagProduction crash collection, JS + native + symbolication

The everyday workflow:

  1. React DevTools for component / state issues.
  2. Hermes debugger or Chrome DevTools for JS logic.
  3. 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 automatically

What 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.js and uploaded to crash reporters.
  • For Sentry, the @sentry/react-native package handles upload automatically when configured with auth-token and org/project.

Hermes-specific gotchas

  • Function.prototype.toString returns 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.log also 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:

  1. Reproducing in debug mode.
  2. Logging at the entry and exit of every native module call.
  3. 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' where X is a named export (should be import { 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 onPress is undefined (typo or missing prop).
  • The view has hitSlop issues — 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 FlatList item).
  • 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's getItemLayout if rows are uniform height.
  • Use the image library'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:

  1. JS errors — RN's global handler (above) or Sentry's automatic wrap.
  2. Native iOS crashes — dSYMs uploaded to the reporter.
  3. 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 --profile flags 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.
  • flashlight for 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.log in 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 isMounted or 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.

On this page