text
$ npx react-native run-ios –scheme “FlagshipApp”
info Found Xcode workspace “FlagshipApp.xcworkspace”
info Building (fe)
error Failed to build iOS project. We ran “xcodebuild” command but it exited with error code 65. To debug build logs further, consider building your app via Xcode.app directly.
** BUILD FAILED **
The following build commands failed:
PhaseScriptExecution [CP-User]\ Generate\ Specs /Users/admin/Library/Developer/Xcode/DerivedData/FlagshipApp-abc/Build/Intermediates.noindex/Pods.build/Debug-iphonesimulator/FBReactNativeSpec.build/Script-001.sh (in target ‘FBReactNativeSpec’ from project ‘Pods’)
(1 failure)
/Users/admin/.rvm/gems/ruby-3.2.2/gems/cocoapods-1.15.2/lib/cocoapods/executable.rb:167:in execute_command':pod’
/usr/local/bin/pod: line 2: /usr/local/Cellar/cocoapods/1.13.0/libexec/bin/pod: No such file or directory
(RuntimeError)
from /Users/admin/.rvm/gems/ruby-3.2.2/gems/cocoapods-1.15.2/lib/cocoapods/executable.rb:52:in
from /Users/admin/projects/flagship/node_modules/react-native/scripts/cocoapods/utils.rb:291:in get_react_native_path'block in (root)’
from /Users/admin/projects/flagship/ios/Podfile:14:in
Error: Failed to install CocoaPods dependencies for iOS project, which is required for this architecture.
Check your Ruby version (currently 3.2.2) and ensure it matches the project’s .ruby-version.
Also, check if ‘ffi’ gem is installed for M1/M2 compatibility.
The output above is the sound of a $4,000 machine choking on its own vomit. It’s 2:14 AM. The "visionary" CTO is likely asleep in a weighted blanket, dreaming of "write once, run everywhere" efficiencies. Meanwhile, I’m staring at a Ruby stack trace inside a JavaScript project that’s trying to compile C++ code to run on an ARM-based simulator.
The failure is predictable. We updated a single transitive dependency—a minor bump in a library that handles SVG rendering—and the entire house of cards folded. CocoaPods is looking for a version of itself that was uninstalled three months ago. The `FBReactNativeSpec` script failed because the environment variables exported by the Metro bundler didn't play nice with the shell environment inherited by Xcode. This is the reality of **react native** in a professional environment. It isn't a framework; it’s a collection of shims held together by duct tape and hope.
## 1. The 3 AM Build Failure: A Forensic Analysis
When you build a native app in Swift, the compiler is your friend. It’s a strict friend, sure, but it’s honest. In **react native**, the build process is a multi-layered nightmare of abstraction. You aren't just compiling code; you are orchestrating a fragile dance between the Node.js runtime, the Ruby-based CocoaPods dependency manager, the Gradle build system for Android, and the LLVM compiler for iOS.
The error log above highlights the first great lie of cross-platform development: that you can escape the native platform. To fix that "error code 65," I have to manually clear the DerivedData folder—a ritual as old as time—but then I have to go deeper. I have to check if the `node_modules` resolution for the `FBReactNativeSpec` is pointing to the right path. Because **react native** 0.73.x uses a "New Architecture" (which has been "new" for three years now), it relies on `Codegen`.
Codegen is supposed to generate C++ interfaces from your TypeScript definitions. It’s a noble goal. But when it fails, it doesn't give you a syntax error. It gives you a 400-line dump of Ruby errors because the script that triggers the C++ generation couldn't find the right version of the `ffi` gem. So here I am, a senior engineer, debugging a Ruby environment mismatch so that I can generate C++ code that I didn't write, to support a JavaScript bridge that shouldn't exist.
The "visionary" CTO thinks this saves time. He sees the "Fast Refresh" feature and thinks we’re moving at light speed. He doesn't see the four hours I spent yesterday trying to figure out why the Android build failed with a `D8: Cannot fit requested classes in a single dex file` error because the **react native** core and its bloated dependency tree pushed us over the 64k method limit. We had to enable multi-dex, which added another 30 seconds to every cold build. The "saved time" is a myth. We just traded stable compile-time checks for runtime volatility and build-time insanity.
## 2. The Hermes Myth and the Reality of Garbage Collection
We were told Hermes would solve everything. "A JavaScript engine optimized for fast start-up on mobile devices," the documentation says. It pre-compiles JavaScript into bytecode. Great. But let’s talk about what happens when the app actually runs.
In a native Swift app, I have ARC (Automatic Reference Counting). I know exactly when an object dies. In Kotlin, I have the ART (Android Runtime) which, while it has a GC, is tuned for the hardware it lives on. In **react native**, we have Hermes.
The problem is the "Stop the World" garbage collection. When you’re scrolling a complex list—say, a feed with high-res images and nested comments—the JS thread is already gasping for air. It’s calculating the virtual DOM diff, it’s handling touch events, and it’s trying to format timestamps. Then, the Hermes GC decides it’s time to clean up the 15,000 small objects created during the last three seconds of scrolling.
```text
// Logcat output during a scroll event
02:16:44.123 D/Hermes: GC Stats (Non-contiguous):
- Total Footprint: 142MB
- Allocated: 98MB
- Collection Time: 42ms (STOP THE WORLD)
02:16:44.165 W/ReactNative: [JSI] Slow block on JS thread: 45ms
02:16:44.166 I/Choreographer: Skipped 3 frames! The application may be doing too much work on its main thread.
Forty-two milliseconds. That’s nearly three frames of animation lost. To the user, it’s a “micro-stutter.” To me, it’s a failure of the fundamental architecture. You can’t “optimize” your way out of a garbage collector that doesn’t know about the native UI lifecycle. The JS thread is decoupled from the UI thread, which sounds good on paper until you realize that any synchronization requires crossing the bridge or using the JSI (JavaScript Interface).
If I want to sync a scroll position with an animation, I have to use react-native-reanimated, which essentially injects C++ code to bypass the JS thread entirely. Think about that. We are using a JavaScript framework, but to make it performant, we have to write code that avoids using JavaScript. It’s architectural insanity.
Table of Contents
3. Dependency Hell: Where SemVer Goes to Die
In the native world, I worry about Podfile.lock or build.gradle. In react native, I live in a node_modules abyss that contains 45,000 files and weighs 1.2GB.
The real nightmare is the transitive dependency conflict. Last week, we tried to update a library for the camera. That library depended on react-native-svg ^13.0.0. But our analytics library was pinned to react-native-svg 12.x because of a breaking change in the way gradients are rendered. In a native project, I could potentially namespace these or handle the conflict with clear compiler warnings. In the Node ecosystem, I get npm list errors or, worse, a runtime crash because two different versions of the same native module are trying to register with the same name.
Invariant Violation: Native module cannot be registered twice: RNSVGNode
at Object.<anonymous> (NativeModules.js:123)
at Module._compile (module.js:650)
at Object.Module._extensions..js (module.js:661)
This “Invariant Violation” is the react native version of a middle finger. It tells you nothing about where the conflict is. You end up using patch-package to manually edit the source code of a library in your node_modules folder, praying that the next developer who runs npm install doesn’t accidentally blow away your fix.
We are currently maintaining 14 different patches for 14 different libraries. We are essentially maintaining our own fork of the entire ecosystem because nobody updates their native bindings fast enough when a new version of react native drops. When 0.73.x came out with Gradle 8 support, half the ecosystem broke because they were still using compile instead of implementation in their build.gradle files. I spent three days rewriting the build scripts for a third-party library that hasn’t been touched since 2019. This is not “feature development.” This is digital archeology.
4. The Bridge is a Bottleneck, and the New Architecture is a Construction Site
The “Bridge” is the central artery of react native. Every time you press a button, a JSON message is serialized, sent across the bridge, deserialized on the native side, processed, and then a response is sent back.
Imagine trying to run a marathon, but every time you take a step, you have to call a translator, tell them you moved your left foot, wait for them to translate that into a different language, and then wait for a confirmation that it’s okay to move your right foot. That’s the bridge.
The “New Architecture” (Fabric and TurboModules) was supposed to kill the bridge. It uses JSI to allow JavaScript to hold a direct reference to C++ host objects. It’s faster. It’s “synchronous.” But it’s also a construction site.
To use TurboModules, you have to deal with Codegen. You have to write your specs in TypeScript or Flow, then run a script that generates the C++ boilerplate. If you make a mistake in your spec, the error message you get is a C++ template deduction failure that looks like this:
In file included from FlagshipApp-generated.cpp:10:
./FlagshipAppSpecs.h:45:12: error: no matching member function for call to 'fromJs'
return fromJs(rt, value);
^~~~~~
./ReactCommon/jsi/JSIDynamic.h:21:12: note: candidate function not viable: requires 2 arguments, but 1 was provided
I’m a Swift developer. I’m a Kotlin developer. I shouldn’t be debugging C++ template mismatches in a generated header file because I forgot to mark a property as “optional” in a TypeScript interface. The complexity has tripled. We now have to understand the entire stack from the JS runtime down to the C++ layer, and if anything breaks in between, the “visionary” CTO is nowhere to be found. He’s at a conference talking about “developer velocity.”
5. Styling in a Sandbox: Why Flexbox in JS is a Necessary Evil
Let’s talk about Yoga. Not the exercise, but the C++ layout engine that powers react native. Yoga implements a subset of CSS Flexbox. It’s the reason we can write flexDirection: 'row' and have it work on both platforms.
But Yoga is a sandbox. It doesn’t know about native UI components. It doesn’t know about UIStackView or ConstraintLayout. It calculates everything in its own little world and then “shims” the results onto native views.
The result? “Jank.”
If you have a deeply nested view hierarchy, Yoga has to calculate the layout for every single node on every frame. In a native app, I can use Auto Layout or Compose’s efficient measurement passes. In react native, I’m often forced to flatten my component tree to the point of unreadability just to keep the frame rate at 60fps.
And don’t get me started on shadows. On iOS, shadows are easy. On Android, react native uses the elevation property, which looks like garbage and offers zero control over shadow color or spread. To get decent shadows on Android, you have to use a third-party library that wraps a native Canvas and draws the shadow manually.
// The "Clean" way to do shadows in React Native
const styles = StyleSheet.create({
card: {
...Platform.select({
ios: {
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.8,
shadowRadius: 2,
},
android: {
elevation: 5, // This is all you get. Deal with it.
},
}),
},
});
This is the “write once, run everywhere” promise in a nutshell: you write it once, then you spend three days writing Platform.select statements to fix the fact that it looks different on every device. You aren’t building a native app; you’re building a simulation of a native app.
6. Survival Strategies for the Reluctant Contributor
If you’re stuck in this basement with me, here is how you survive.
First, ignore the “official” documentation when it comes to environment setup. The official docs assume you have a pristine machine. You don’t. You have three versions of Ruby, four versions of Node, and an Android Studio installation that’s currently trying to download 4GB of system images. Use asdf or nvm to pin your versions. If you don’t pin your versions, the project will die the moment someone else clones it.
Second, embrace patch-package. It is the only way to stay sane. When you find a bug in a library—and you will find bugs—don’t wait for a PR to be merged. It won’t be. The maintainer probably burned out in 2021. Fix it in your node_modules, run npx patch-package, and commit the diff.
Third, treat the bridge with suspicion. If you’re sending more than a few kilobytes of data across the bridge per second, you’ve already lost. If you’re trying to do high-frequency updates (like a timer or a progress bar), don’t do it in JS. Use a native driver for animations.
Fourth, keep your native skills sharp. You will need them. You will eventually have to write a TurboModule in Objective-C++ or Kotlin to do something “simple” like accessing the file system or handling a specific Bluetooth profile. If you forget how the native platforms work, you’ll be at the mercy of whatever wrapper some random person on GitHub decided to write.
Finally, don’t trust the Metro bundler. It will lie to you. It will tell you it has refreshed your code when it’s actually serving a cached version of a file from three hours ago. When in doubt, kill the process, clear the cache (npx react-native start --reset-cache), and rebuild. It’s the only way to be sure.
Post-Mortem: The Bitter Truth
It’s 3:45 AM now. The build finally finished. The app is running on the simulator. It looks… fine. To the average user, it’s a functional app. They won’t notice the micro-stutter in the navigation transition. They won’t see the 140MB of memory being wasted by the Hermes runtime. They won’t know that the “Submit” button works by serializing a JSON object and shouting it across a C++ bridge.
Why am I still using react native? Why haven’t I quit and joined a “pure” Swift shop?
Because, despite the technical debt, despite the “jank,” and despite the fact that I’m currently debugging a Ruby script to fix a JavaScript app, the business doesn’t care about the “how.” They care about the “when.”
With react native, we shipped a feature to both platforms in two weeks. If we had done it natively, it would have taken four weeks and two different teams. The “visionary” CTO is right about the economics, even if he’s fundamentally wrong about the engineering.
I hate this framework. I hate the way it abstracts away the beauty of the underlying platforms. I hate the node_modules folder. But I’m still here, in the dark, because at the end of the day, my job isn’t to write beautiful code; it’s to ship. And react native, for all its flaws, is the fastest way to ship a mediocre product to two platforms at once.
Now, if you’ll excuse me, I have to go figure out why the Android build is failing with a java.lang.OutOfMemoryError: Metaspace. I think I’ve only got three more coffee cups left in me.
Audit Status: FAILED.
Survival Status: MARGINAL.
Project Status: SHIPPED (mostly).
Related Articles
Explore more insights and best practices: