+1 (415) 943-4271

Debugging React Native in 2026: React Native DevTools, Radon IDE, and Reading a Native Crash

Most React Native teams we walk into have a debugging story that stopped being true two years ago. Someone still has debugger statements aimed at a Chrome tab that no longer attaches, someone else has a Flipper plugin in Podfile that nothing loads anymore, and everybody debugs with console.log because that is the one thing that never breaks.

The tooling actually got better. Chrome Remote Debugging (the old "Debug JS Remotely") and Flipper are both out of the picture; React Native DevTools, built on the Chrome DevTools Protocol and talking directly to Hermes, is the default. It is a real debugger against the engine your app actually runs, not a proxy running your JavaScript in V8 in a browser tab with different timing and no native modules. This post is how we use it on client projects, and where it still leaves you blind.

Why the old setup lied to you

The reason remote debugging had to die is worth knowing, because it explains a whole class of "works in debug, breaks in release" bug reports:

  • Under remote debugging your JS ran in Chrome's V8, not Hermes. Different engine, different JS semantics at the edges, different Intl support, different stack traces.
  • Every bridge call became an asynchronous WebSocket round trip. Anything timing-sensitive — gesture handling, animation callbacks, startup sequencing — behaved completely differently with the debugger attached.
  • Synchronous native calls simply could not work, which on the New Architecture (where JSI and Turbo Modules are synchronous by design) is fatal.

So the first rule: if your team is still reaching for a browser tab, you are debugging a different app than the one you ship.

Opening React Native DevTools

With the dev server running, press j in the Metro terminal, or open the Dev Menu in the app (shake the device, d in the terminal, Cmd+D on the iOS simulator, Cmd+M / Ctrl+M on Android emulators) and choose Open DevTools. It works on simulators, emulators, and physical devices on the same network. It only attaches to Hermes in a debug build — a release build has no inspector target by design.

What you get:

  • Console — a real REPL evaluating inside Hermes, including await at the top level. $0-style selection binding works with the React DevTools integration.
  • Sources — breakpoints, conditional breakpoints, logpoints, step in/over/out, watch expressions, and a call stack that is source-mapped back to your TypeScript.
  • Memory — heap snapshots from Hermes. This is the tool for leak hunting; more on that below.
  • React Components and React Profiler — the React DevTools panels, embedded.

Breakpoints that survive a reload

The practical workflow that most people miss: breakpoints set in the Sources panel persist across Fast Refresh, and you can pause on the very first frame of a reload. If the bug is in startup — a provider that mounts twice, a token read before hydration finishes — set the breakpoint, then reload with r in Metro. You do not need to time anything by hand.

Logpoints are underrated for anything animation- or list-adjacent. Right-click the gutter, choose Add logpoint, and type an expression. You get console.log output without editing the file, without triggering a refresh, and without shipping a stray log into a PR.

Pause on exceptions, specifically caught ones

In a codebase with a lot of try/catch around network calls, turn on Pause on caught exceptions temporarily. Half of the "the screen is just blank" tickets we get handed are an error being swallowed three layers down and rendered as an empty state. The debugger finds it in about ninety seconds; reading the code finds it in an afternoon.

The React Profiler when the React Compiler is on

If you have adopted the React Compiler, the Profiler flame chart now annotates memoized components. That changes how you read it. A component that re-renders despite compiler memoization is telling you one of a few things:

  • The component bailed out of compilation (check for the "use no memo" directive or an ESLint compiler rule violation in that file).
  • Its props genuinely changed — often an inline object or array created in a parent that the compiler could not hoist because it closes over something mutable.
  • Context. The compiler does not save you from a context value that changes identity on every render; split the context or memoize the value.

Record a profile of the actual bad interaction, sort by self time, and only then start changing code. We have lost count of the number of useMemo calls added to components that never showed up in a profile at all.

Finding leaks with heap snapshots

Hermes heap snapshots load in the Memory panel and behave like browser ones. The workflow that works:

  1. Navigate to the screen, then back out. Do it twice to let lazy caches settle.
  2. Take a snapshot.
  3. Push and pop the screen five more times.
  4. Take a second snapshot and use Comparison view.

If instances of your screen component, or of the objects it closes over, grow linearly, you are holding a reference. The usual suspects in React Native: an event subscription never removed (addListener without the matching remove()), a setInterval that outlives the screen, an animation frame loop that keeps a stale worklet closure alive, and image or video players kept in a module-level cache that nothing evicts.

Network and storage inspection

This is where teams miss Flipper the most. Options, roughly in order of how often we reach for them:

  • The Network panel in React Native DevTools. It covers fetch and XMLHttpRequest, which is what most apps use through Axios or a query client. It does not see requests made from native code — an image loader, a native SDK, an analytics library — so an empty panel does not mean nothing happened.
  • Reactotron. Still the cheapest way to get a persistent, filterable timeline of requests, state changes, and custom logs sitting next to the app rather than inside a DevTools tab. Easy to wire to Redux, Zustand, or MobX.
  • A proxy (Proxyman, Charles, mitmproxy). The only thing that sees everything, including native SDK traffic and TLS details. Budget an hour for cert installation on each platform, and remember that Android needs a network security config permitting user CAs in debug builds only.
  • Storage. For MMKV, expose a dev-only screen that dumps keys, or use the Reactotron MMKV plugin. For SQLite-backed stores (Drizzle, op-sqlite, WatermelonDB), pull the database file off the simulator and open it in a SQLite browser — far faster than writing debug queries.

Radon IDE for people who live in the editor

Radon IDE is a VS Code / Cursor extension that puts the simulator inside the editor. It matters for a few workflows that were previously painful:

  • Click-to-inspect: click an element in the embedded device view and jump to the JSX that rendered it. This alone repays the setup cost on an unfamiliar codebase.
  • Breakpoints in your editor gutter that just work, without a separate DevTools window.
  • Router integration: jump directly to a deep route instead of tapping through five screens to reproduce, which is a huge deal for Expo Router apps with auth-gated stacks.
  • Device settings — dark mode, text size, locale, permissions — toggled from a panel, which makes accessibility and localization passes far less tedious.

It is a commercial product with a free tier, so it is a team decision rather than a default. Our rule of thumb: worth it for anyone doing daily feature work in an app with complex navigation; unnecessary for someone dipping in once a month.

Triage before you debug

A fast triage step saves more time than any tool. When a bug lands, decide which layer it belongs to before you open anything:

SymptomLikely layerFirst tool
Red LogBox with a JS stackJavaScriptDevTools Sources, pause on exceptions
Blank screen, no errorJavaScript, swallowedPause on caught exceptions
Jank during scroll or gestureRendering / workletsReact Profiler, then platform trace
Crash with no JS stack, app diesNativeDevice logs and symbolicated crash report
Works in debug, fails in releaseBuild config / minificationRelease build with source maps

That last row deserves its own habit: reproduce in a release build before you spend a day in the debugger. Dead code elimination, different __DEV__ branches, ProGuard/R8 rules, and missing config plugin side effects all only show up there.

When the failure is native

JavaScript tooling shows you nothing when the process dies. Go to the platform:

  • iOS: run the app from Xcode and read the console, or use xcrun simctl spawn booted log stream --level debug for simulators. Crash reports live in Xcode > Window > Devices and Simulators > View Device Logs, and they need symbolication against the matching dSYM to be readable. Enable the Address Sanitizer or Zombie Objects schemes if you suspect memory misuse from a native module.
  • Android: adb logcat with a filter is still the fastest path — adb logcat *:E ReactNativeJS:V gets you errors plus JS logs. Native crashes produce a tombstone with a stack of memory addresses; run it through ndk-stack with your unstripped symbols. adb shell dumpsys gfxinfo <package> gives you frame timing when the complaint is "it feels slow" and nobody can be more specific.
  • Either platform: if the crash only happens for users, your local tooling is the wrong instrument entirely. That is a source-map and crash-reporting problem, and we covered it in Observability for React Native.

Debugging on the New Architecture, specifically

Two things change with Fabric and Turbo Modules:

  • Synchronous calls now really are synchronous. A native module that blocks will block your JS thread visibly. If you see a frozen UI with no error, check for a synchronous Turbo Module method being called on every render.
  • Stack traces cross the boundary better than they used to. A JS error thrown out of a Turbo Module now usually carries the native frames. Read the whole stack before assuming it is your code.

Also: worklets from Reanimated run on a separate runtime. console.log inside a worklet is forwarded, but breakpoints in worklet code are unreliable. Debug the logic by lifting it into a plain function you can test, and keep the worklet body as thin as possible — which is good practice anyway.

A short checklist to standardize on

If you want one thing out of this post, make it a written team convention:

  1. Debug builds attach with j from Metro. Nobody uses a browser tab.
  2. Pause-on-caught-exceptions is the first move for blank screens.
  3. Every performance claim comes with a Profiler recording or a platform trace attached to the ticket.
  4. Leaks are proven with two heap snapshots, not asserted.
  5. Any "only in production" bug gets reproduced in a local release build before anyone opens the debugger.
  6. Native crashes go to logcat or Xcode device logs, symbolicated, before anyone edits JavaScript.

Where we come in

We get called in a lot for the bugs that survived a team's normal process — the intermittent crash that only shows on one Android OEM, the startup regression nobody can bisect, the memory growth that only appears after twenty minutes of use. Usually the fix is small; finding it is the work, and finding it is mostly tooling discipline.

If your team is spending more time reproducing bugs than fixing them, get in touch. We do short debugging and performance engagements as well as longer-term React Native consulting.