Things I learned: transpiling React Native JavaScript with babel in 2025

At work we’re using React Native to build our Android and iOS app. We use it with Hermes, a JavaScript engine Facebook/Meta built for React Native, which doesn’t have a JIT compiler, but is instead optimized for fast startup, by compiling JavaScript to a type of bytecode during build time.

Hermes supports plenty of modern JavaScript features, yet any current React Native app will have its source code processed by a bunch of babel plugins, transforming modern JS code into not-so-modern one.

In our case this bothered me in particular when debugging events handlers that make use of JavaScript’s async/await feature. Hermes supports this natively, but one of the babel plugins included in the build process, would replace each instance with generators (also a relatively new JS feature, but not quite as new as async/await). The resulting stacktrace, if not properly mapped to the original source code, is much harder to read, since there’s usually two extra items in the stack, one referring to a function injected by babel called _asyncToGenerator.

Trying to stop this transform from happening turned into quite a deep rabbit whole. I’ll summarize here what we learned:

  1. our babel.config.js uses babel-preset-expo as the only present
  2. babel-preset-expo in turn uses @react-native/babel-preset as a preset
  3. @react-native/babel-preset includes @babel/plugin-transform-async-to-generator as a plugin
  4. @babel/plugin-transform-async-to-generator itself transform async functions to generators!

To figure out this chain of dependencies, npm ls was useful:

npm ls @babel/plugin-transform-async-to-generator
└─┬ [email protected]
  └─┬ [email protected]
    └─┬ @react-native/[email protected]
      └── @babel/[email protected]

None of these presets have any configuration options we could find, so in the end we decided to add another patch with patch-package:

diff --git a/node_modules/@react-native/babel-preset/src/configs/main.js b/node_modules/@react-native/babel-preset/src/configs/main.js
index 077e1a6..d27d775 100644
--- a/node_modules/@react-native/babel-preset/src/configs/main.js
+++ b/node_modules/@react-native/babel-preset/src/configs/main.js
@@ -133,7 +133,7 @@ const getPreset = (src, options) => {
     extraPlugins.push([
       require('@babel/plugin-transform-async-generator-functions'),
     ]);
-    extraPlugins.push([require('@babel/plugin-transform-async-to-generator')]);
+    // extraPlugins.push([require('@babel/plugin-transform-async-to-generator')]);
   }
   if (
     isNull ||

This is now the 10th patch we’re maintaining, despite using renovate for greenkeeping.

After we verified our app still works fine without this transform, I figured I can at least ask if this could be upstreamed to @react-native/babel-preset. In short: no, at least not yet. From the community discussion post:

  • @react-native/babel-preset needs to support JSC (ie Safari on iOS >= 15.4, and the version of JSC we build for Android) as well as Hermes, so support needs to be universal before we remove plugins completely, but we can gate individual plugins by target (isHermes).
  • There may be runtime performance implications, we can check with the Hermes team or run an experiment case-by-case on that.
  • Static Hermes is coming soon-ish, which will have a very different level of language support and performance characteristics, so it might make sense to hold off for that.
discussions-and-proposals, Remove @babel/plugin-transform-async-to-generator from @react-native/babel-preset

Maybe in a year or so.

If you liked this post, boost it on Mastodon. Please reply there with comments, questions or ideas.

-Jörn