DEV Community

Cover image for The Hidden Failure Modes of React Native TurboModules and Codegen
Amit chakraborty
Amit chakraborty

Posted on Originally published at amitchakraborty.dev

The Hidden Failure Modes of React Native TurboModules and Codegen

I was leading the architecture for a clinical AI platform at Synapsis Medical Technologies, where we were integrating real-time health data from wearables into a React Native application. We were moving to the New Architecture (React Native 0.7x) specifically to use TurboModules for high-frequency data processing. On paper, the synchronous bridge-less communication was exactly what we needed to handle raw sensor streams without dropping frames.

In development, everything was fluid. But as soon as we pushed to our internal alpha—running on a mix of older Android devices and newer iPhones—the crashes started. Not just JavaScript exceptions, but silent C++ termination signals and SIGSEGV errors that gave us zero stack traces in Sentry. We saw a 12% increase in "App Not Responding" (ANR) reports on Android within the first six hours.

The cost was immediate: we had to roll back the entire release, delaying our clinical validation study by a week. What the documentation doesn't tell you is that while Codegen handles the boilerplate of generating C++ headers, it creates a rigid contract that, if violated by even a single null byte or an unhandled type mismatch in the native implementation, will take down the entire process.

Why Codegen Fails Under Load

The React Native bridge was forgiving. If you sent a slightly malformed JSON object over the bridge, the serialisation layer usually caught it, or the JS side received undefined. TurboModules are different. They use JSI (JavaScript Interface), which means your JavaScript is calling C++ functions directly.

The failures I’ve seen in production usually stem from three things:

  1. Strict Type Enforcement: If your NativeSampleModule.ts defines a return type as string, but your Objective-C++ or C++ implementation returns nil or a nullptr under a specific error condition, the app will crash instantly. JSI does not "fail gracefully" to undefined.
  2. Thread Safety: Moving to TurboModules often tempts engineers to perform heavy lifting on the JS thread because the overhead is lower. However, if you block the JSI call with a synchronous long-running C++ operation, you freeze the UI.
  3. Codegen Desync: In a CI/CD environment, if your generated C++ files (the build folder artifacts) don't perfectly match the TypeScript spec because of a caching layer in your runner, you end up with binary incompatibility that only manifests as a crash at runtime.

The Fix: Hardening TurboModules Step-by-Step

To move from a fragile implementation to one that survives 99.9% uptime in a clinical environment, follow these steps.

1. Enforce Null-Safety in the Spec

Never assume the native side will always have data. The biggest mistake is defining a tight spec that doesn't account for native failures.

What to do: Open your Native<MyModule>.ts file. If a value can ever be missing (e.g., a sensor reading, a database result), mark it as optional.

// NativeMyModule.ts
import type { TurboModule } from 'react-native';
import { TurboModuleRegistry } from 'react-native';

export interface Spec extends TurboModule {
  // Instead of: getSensorData(): string;
  getSensorData(): string | null; 
}

export default TurboModuleRegistry.getEnrolled<Spec>('MyModule');
Enter fullscreen mode Exit fullscreen mode

Why: This forces Codegen to generate std::optional<std::string> in C++ and NSNumber * or NSString * that allows for nil in Objective-C.
Confirm: Run npx react-native codegen. Check the generated MyModuleSpec.h file. Look for std::optional in the method signature.

2. Implement the "Result" Pattern in C++

JSI crashes often happen when an exception is thrown in C++ and not caught before it hits the JS boundary.

What to do: In your native implementation (e.g., MyModule.mm or MyModule.cpp), wrap your logic in a try-catch block and return a structured object instead of a raw value.

// Objective-C++ implementation
- (NSDictionary *)getData {
  try {
    // Dangerous native logic
    return @{@"status": @"success", @"data": result};
  } catch (NSException *exception) {
    return @{@"status": @"error", @"message": exception.reason};
  }
}
Enter fullscreen mode Exit fullscreen mode

Why: It is safer to parse an error object in JS than to handle a C++ signal.
Confirm: Trigger a known failure on a physical device. The app should remain responsive and log the error to your JS console.

3. Sanitise CI/CD Build Artifacts

I have seen release cycles delayed because a Jenkins or GitHub Actions runner reused a build directory where node_modules were updated but the generated C++ code wasn't regenerated.

What to do: In your build scripts, explicitly delete the generated folders before running the build.

# In your CI script
rm -rf android/app/build
rm -rf ios/build
rm -rf node_modules/react-native/ReactAndroid/src/main/jni/generated
# Now run the build
npx react-native run-android --variant=release
Enter fullscreen mode Exit fullscreen mode

Why: This ensures that the C++ headers used by the compiler are exactly what your current TypeScript specs define.
Confirm: Check the timestamps of the files in android/app/build/generated/source/codegen to ensure they were created during the current job.

What it costs and when to avoid it

TurboModules are not a "free" performance upgrade.

  • Binary Size: Adding TurboModules increases your binary size. In my experience, adding the New Architecture and a few complex TurboModules adds roughly 3MB to 5MB to the compressed APK/IPA. This is due to the C++ runtime and the generated glue code.
  • Compilation Time: Your build times will increase. Expect an additional 2 to 4 minutes on a clean CI build because the compiler now has to process the generated C++ files and link them.
  • Complexity: If your team is purely JavaScript-focused, the first time a TurboModule fails, they will be stuck. You need at least one engineer comfortable with C++ and the JSI memory model.

Do not use TurboModules if: You are just making standard REST API calls or doing basic CRUD. The overhead of maintaining the C++ layer outweighs the microsecond performance gains. Use them only for high-frequency data (60fps+), heavy cryptography, or direct hardware manipulation.

At your level

Starting out

If you are a junior engineer or a student, focus on the TypeScript spec. Understand that the NativeMyModule.ts is a contract. If you change a parameter name there, you must re-run Codegen and update the native code immediately, or the project will not compile.

Working engineer

You should be monitoring your crash reporting tool for EXC_BAD_ACCESS or SIGSEGV. If these spike after a TurboModule rollout, it is almost certainly a null-pointer dereference in your C++ or Objective-C++ code where you ignored the "optional" flag in the spec.

Senior or staff

Own the build pipeline. Ensure that Codegen is integrated into the pre-commit or CI flow so that no one can push a JS spec change without the corresponding native updates. You should also be evaluating if a feature actually needs a TurboModule; often, a well-optimised legacy bridge module is safer for non-critical paths.

Lead or director

Account for the "Native Tax." When planning a migration to the New Architecture, add 20% to your sprint estimates for the first three months to account for the steeper learning curve of C++ debugging and the increased build complexity.

In the interview

The Question: "We are seeing intermittent crashes on our New Architecture app that don't show up in our JS error logs. How do you debug this?"

The Weak Answer: "I would check the console logs or add more try-catch blocks in my JavaScript code." This is weak because JSI/C++ crashes bypass the JavaScript engine entirely; the JS environment is destroyed before the log can be written.

The Strong Answer: A strong candidate will immediately identify this as a native-level crash (likely a SIGSEGV or abort() call). They will talk about:

  1. Symbolicating Native Stacks: Using dsym for iOS or mapping.txt for Android to read the C++ stack trace.
  2. JSI Type Mismatch: The failure of a native method to return the type guaranteed by the Codegen spec.
  3. Memory Management: How C++ objects held by the JSI Runtime might be garbage collected in JS while still being accessed in C++, leading to use-after-free errors.

The Senior Follow-up: "How do you handle high-frequency data without blocking the UI thread in a TurboModule?"
The candidate should explain that while TurboModules allow synchronous calls, heavy work should still be dispatched to a background thread (like dispatch_async in GCD or a custom thread pool in C++), and results should be returned via a JS callback or a Promise to keep the JSI call itself near-instant.


Amit Chakraborty is a founding engineer and senior architect — React Native, AI/RAG systems and production architecture. Portfolio: www.amitchakraborty.dev · LinkedIn · GitHub. Open to senior and founding engineering roles, remote worldwide.

Top comments (0)