Debugger Stops on “Ghost” Breakpoints

Introduction

When debugging React Native applications, you may expect the debugger to pause execution precisely where they place breakpoints within your code. However, a recurring issue has emerged where the debugger appears to stop on lines that the developer did not explicitly mark—especially within libraries or non-user code. This behavior undermines debugging precision and causes confusion and inefficiency during development.

What is the Problem?

The core problem is the appearance of so-called “ghost breakpoints”: these are debugger pauses on lines of code that were not marked by the user. These breakpoints usually appear in auto-generated or transpiled sections of the codebase, not in the original source files. This makes debugging asynchronous flows—especially those using async/await or Promises—particularly misleading and frustrating.

For example, when you place a breakpoint on an async function:


and you step over it, the debugger will take you to the asyncGenerator code:

What Causes the Problem?

The root of the issue lies in the transpilation process used in React Native applications, which are typically written in TypeScript or modern JavaScript and then converted (or transpiled) into plain JavaScript using tools like Babel.

How Transpilation Works

React Native codebases are usually written in TypeScript or modern JavaScript (ES6+), which includes syntax and features not natively supported in all JavaScript environments. Tools like Babel are used to transpile this code into plain JavaScript to ensure broad compatibility. This process involves:

  • Transforming modern constructs like async/await, arrow functions, classes, etc., into older JavaScript that can run in more environments.
  • Injecting polyfills for features like Promises or generators if the target runtime lacks native support.
  • Converting async/await into generators using Babel plugins like @babel/plugin-transform-async-to-generator.

For instance, the following code:

// ts
async function fetchData() {
const response = await fetch('/data');
return response.json();
}

Is transpiled into something closer to:

// js
function fetchData() {
return regeneratorRuntime.async(function fetchData$(_context) {
while (1) switch (_context.prev = _context.next) {
case 0:
_context.next = 2;
return fetch('/data');
case 2:
return _context.abrupt("return", _context.sent.json());
}
});
}

This transformation introduces additional wrapper functions, control flow logic, and calls to injected helpers like regeneratorRuntime. These elements do not correspond directly to lines in the original TypeScript or JavaScript source, complicating debugger mapping.

Where the Problem Originates

When debugging, the JS engine steps through the transpiled code, not the original source. While source maps are intended to map these positions back to your original TypeScript, this mapping breaks down with complex constructs like generators or async functions, especially across multiple Babel plugins and toolchains (e.g., Metro, Hermes).

The debugger relies on source maps to map transpiled code back to the original source code. However, when transformations like generator functions and Promise wrapping are involved, these maps can become inaccurate or ambiguous, especially in deeply nested or chained async logic. As a result, breakpoints may “drift” into these helper functions, creating the illusion of ghost breakpoints.

This is particularly problematic in the composed pipeline that uses Babel, Hermes, and other transformations. Known limitations in source maps with complex async constructs contribute heavily to this issue.

Known Issue in the React Native Community

This debugging behavior is widely known across various tools and platforms that make up the React Native development pipeline. Developers using Babel (especially with @babel/plugin-transform-async-to-generator and related plugins) have experienced challenges in maintaining accurate source mappings when debugging asynchronous code.
Hermes, the JavaScript engine, has made some progress on improved source map handling, but the problem persists.

Many discussions and GitHub issues reference this problem:

This post by rn Zaefferer summarized the state of the art of JS debugging in 2025:

Workarounds

There are a few known workarounds, though each comes with trade-offs:

  1. Disabling async-to-generator transformation: This can prevent complex rewrites, resulting in simpler source maps, but:
  • May break compatibility with older JavaScript runtimes
  • May not be viable depending on project setup
  • Reduces the scope of optimization and transformation
  • Can impact performance and compatibility, discouraged for production builds
  • Debugging experience improvements are limited
  1. Manual debugging without async/await: Refactor code into .then() chains—unrealistic for most codebases.
  2. Source map tuning: Tools like Metro allow tweaking source map generation, but deep async logic remains difficult to map accurately. Furthermore, they are brittle and may break during bundling.

What is the community doing about it?

The broader JavaScript and React Native communities are actively working on this issue:

  • Hermes is improving support for better source map fidelity and more accurate async stack traces. Hermes changelogs reflect ongoing enhancements in debugging.
  • Metro bundler is undergoing active development to better handle source maps across complex async transforms.
  • The React Native core team has acknowledged the pain around debugging and is working on initiatives like better integration with Chrome DevTools and improved error mapping with react-native-error-overlay.
  • Babel is moving toward improving plugin modularity and performance, and async handling is on the roadmap for several tools involved in the transpilation pipeline.

There is no definitive fix today, but potential deprecation of certain Babel transforms offer hope for more accurate and stable debugging in future versions of React Native.

Recommendations

As mentioned before there is no definitive solution to this issue, and the available workarounds are not generic, it depends on the project specific. If you are facing the problem, verify on the above cited discussion if you can apply the proposed solutions.
But keep in mind that most of them will impact the performance of your application and they should not be apply for production builds, and do not apply them without making an analyzes of the impact.