React Native combines JavaScript and native runtimes, which makes troubleshooting powerful but sometimes tricky. The fastest path out of most failures is a consistent, repeatable six-step workflow: inventory your environment and versions, reproduce with a narrow scope, capture the right logs, apply the smallest safe change, verify, and roll back cleanly if needed. This guide provides practical, copy-paste commands and configuration pointers for bare and Expo workflows on Android and iOS. You will learn how to read the right logs, validate network behavior, and fix common issues with module resolution, native builds, REST calls, Google Play Billing, Stripe, and RevenueCat. Each example includes expected results, failure modes, and recovery steps you can trust in day-to-day operations.
TL;DR — 5 Commands That Resolve 80% of Incidents
npx react-native info— capture baseline inventorynpx react-native start --reset-cache— clear Metrocd android && ./gradlew clean assembleDebug --stacktrace— clean Android buildcd ios && pod deintegrate && pod install && cd ..— reset iOS podsrm -rf node_modules && npm ci— universal dependency refresh
Prerequisites & Assumptions
This runbook covers React Native 0.72–0.76 across three workflow flavors: bare workflow, Expo managed workflow, and Expo development client. Host operating systems: macOS (required for iOS), Linux, and Windows. Required toolchain baselines: Node 18 or 20 LTS; single package manager (npm 9+, Yarn 1.x, or pnpm 8+); JDK 17 or 21 aligned with Android Gradle Plugin (AGP) 8.x; Android SDK with platform-tools and build-tools 34+; Xcode 15+ with iOS 15.1+ deployment target; CocoaPods 1.13+; Ruby 3.2+ (via rbenv/rvm/chruby, not system Ruby); Watchman 2024+; fastlane latest if used for CI/CD. Not covered: React Native <0.70, classic architecture-only codebases without Hermes, legacy react-native-cli global installs, or Windows native builds (React Native for Windows is out of scope).
Architecture & Log Flow Diagram (text)
[Metro Bundler] --(JS bundle)--> [Hermes/JSC Runtime] --(JSI)--> [Native Bridge]
- +-- Android: logcat (ReactNative, ReactNativeJS, System.err)
- +-- iOS: OSLog / Xcode Console (subsystem: com.apple.reactnative)
- +-- Hermes bytecode crashes -> /data/data/<pkg>/files/.hermes/*.log
- +-- Metro logs (stdout/stderr, HMR events, resolution graph)
New Architecture (Fabric/TurboModules):
[Codegen] -> [C++/ObjC/Swift/Kotlin generated] -> [Fabric Renderer / TurboModule Registry]
Log paths: Metro (Codegen output), Gradle (generateCodegenArtifactsFromSchema), Xcode (Fabric mount logs), logcat (TurboModule registration)
Environment Inventory (Expanded)
Run the following in the project root to populate an incident header. Copy the template below into your ticket.
# Incident header template — copy output into ticket
echo "=== INCIDENT HEADER ==="
echo "Date: $(date -u +"%Y-%m-%dT%H:%M:%SZ")"
echo "Git commit: $(git rev-parse HEAD 2>/dev/null || echo 'N/A')"
echo "Branch: $(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo 'N/A')"
npx react-native info
echo "--- Expo doctor ---"
npx expo doctor 2>/dev/null || echo "Expo CLI not available"
echo "--- Community CLI doctor ---"
npx @react-native-community/cli doctor 2>/dev/null || echo "Community CLI not available"
echo "--- Gradle ---"
gradle -v 2>/dev/null || (cd android && ./gradlew -v 2>/dev/null) || echo "Gradle not found"
echo "--- Xcode SDKs ---"
xcodebuild -showsdks 2>/dev/null | head -20
echo "--- Ruby ---"
ruby -v && which ruby
echo "--- Watchman ---"
watchman --version 2>/dev/null || echo "Watchman not installed"
echo "--- Fastlane ---"
fastlane --version 2>/dev/null || echo "Fastlane not installed"
echo "--- Node/PM ---"
node -v && (npm -v || yarn -v || pnpm -v)
echo "=== END HEADER ==="
Key inventory items to record: React Native version and template type; JS engine (Hermes default, JSC opt-in); Node and package manager versions with lockfile type; JDK distribution and version, AGP version from android/build.gradle or gradle/libs.versions.toml; Android SDK compileSdk and targetSdk; Xcode version, CocoaPods version, Ruby manager; iOS deployment target from Podfile; Watchman version; fastlane version if applicable.
Safe Configuration Baseline (Expanded)
Adopt these defaults before incident work to reduce blast radius.
Package management: Use exactly one package manager. Commit package-lock.json (npm), yarn.lock (Yarn 1.x), or pnpm-lock.yaml (pnpm). Avoid floating versions (^, ~) in package.json during incidents; pin exact versions.
Metro resolution (metro.config.js):
module.exports = {
resolver: {
unstable_enablePackageExports: true,
sourceExts: ['js', 'jsx', 'json', 'ts', 'tsx', 'mjs'],
// Add custom resolution for monorepos if needed
},
};
React Native config (react-native.config.js):
module.exports = {
assets: ['./assets/fonts'],
project: {
ios: {},
android: {},
},
};
Gradle properties (android/gradle.properties):
org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=512m
org.gradle.parallel=true
org.gradle.caching=true
android.enableJetifier=true
# Hermes default
hermes_enabled=true
Gradle wrapper (android/gradle/wrapper/gradle-wrapper.properties):
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
Podfile (ios/Podfile):
platform :ios, '15.1'
use_frameworks! :linkage => :static # or :dynamic for Swift pods
$RNFirebaseAsStaticFramework = true # if using Firebase
Secrets and config: Never embed secrets (Stripe secret keys, API tokens) in the client. Use react-native-config (bare) or expo-constants with extra (Expo) for environment-specific base URLs and publishable keys. Keep debug-only flags (cleartext, ATS exceptions) out of release builds via build variants/xcconfigs.
Core Reset Commands (Platform Matrix)
| Workflow | Android | iOS | Notes |
|---|---|---|---|
| Bare | cd android && ./gradlew clean assembleDebug --stacktrace | cd ios && pod deintegrate && pod install --repo-update && cd .. | Run adb reverse tcp:8081 tcp:8081 for device debugging |
| Expo Managed | npx expo prebuild --clean --platform android then cd android && ./gradlew clean assembleDebug | npx expo prebuild --clean --platform ios then cd ios && pod install --repo-update | npx expo start -c clears Metro cache |
| Expo Dev Client | eas build --profile development --platform android --local | eas build --profile development --platform ios --local | Development client logs via adb logcat / Xcode Console |
| All | npx react-native start --reset-cache | npx react-native start --reset-cache | Metro cache reset universal |
| Simulator reset | adb emu kill + cold boot | xcrun simctl erase all | Nuclear option for stale state |
Windows PowerShell equivalents:
Set-Content -Path android\local.properties -Value "sdk.dir=$env:ANDROID_SDK_ROOT"
.\android\gradlew.bat clean assembleDebug --stacktrace
npx expo prebuild --clean --platform android
Log Sources & Correlation
| Target | Source | Command / Access |
|---|---|---|
| Metro bundler | Terminal stdout/stderr | npx react-native start --reset-cache |
| Android device logs | logcat (filtered) | adb logcat -s ReactNative:V ReactNativeJS:V System.err:V -v threadtime |
| Android build | Gradle | cd android && ./gradlew assembleDebug --stacktrace --scan |
| iOS runtime | Xcode Console / OSLog | xcrun simctl spawn booted log stream --predicate 'process == "MyApp"' --style compact |
| Hermes crash dumps | Device file system | adb shell run-as com.myapp cat files/.hermes/*.log | npx metro-symbolicate |
| Expo managed | Expo CLI / Metro | npx expo start -c |
| EAS Build | EAS dashboard / CLI | eas build:list --platform=android --limit=5 then eas build:view |
| expo-updates | Device logs | adb logcat -s ExpoUpdates:V / Xcode Console filter ExpoUpdates |
| New Architecture Codegen | Gradle / Xcode | ./gradlew generateCodegenArtifactsFromSchema --stacktrace / Xcode build log Codegen |
Hermes crash analysis: After a native crash on Android, pull the Hermes log and symbolicate:
adb shell run-as com.myapp cat files/.hermes/*.log | npx metro-symbolicate > hermes-crash.txt
# For profiling, enable Hermes profiler:
adb shell setprop debug.hermes.profiler 1
# Then open chrome://tracing and load the generated profile
Troubleshooting Patterns (New)
Codegen / TypeScript Config Drift
Symptom: error: cannot find module 'NativeMyModule' or Codegen fails with TypeScript errors. Cause: react-native-codegen expects strict TypeScript config; js/ts mismatches in spec files. Fix:
# Verify Codegen can parse your specs
npx react-native-codegen --help
# In bare workflow, regenerate
cd android && ./gradlew generateCodegenArtifactsFromSchema --stacktrace
cd ../ios && pod install --repo-update
Expected: Generated C++/Swift/Kotlin files under build/generated/codegen (Android) and Pods/Headers/Private/React-Core/ReactCommon (iOS). Failure: TypeScript strict: false or missing skipLibCheck in tsconfig.json. Recovery: Align tsconfig.json with RN template defaults; ensure react-native-codegen version matches RN version.
TurboModule "Native module cannot be null"
Symptom: JS error Native module cannot be null for a custom TurboModule. Cause: Module not registered in ReactNativeHost (Android) or RCTBridge (iOS), or Package not added. Fix: Verify MainApplication.java / MainApplication.kt includes new MyTurboModulePackage() in getPackages(). On iOS, ensure RCTAppSetupDefaultRootView includes the module or autolinking picked it up (pod install --repo-update). Verification: adb logcat -s ReactNative:V shows TurboModuleRegistry.getEnforcing success.
Fabric "ShadowNode not found"
Symptom: Crash in FabricUIManager with ShadowNode not found for tag. Cause: Version mismatch between react-native and react-native-reanimated / react-native-gesture-handler Fabric components. Fix: Pin react-native-reanimated to version compatible with RN (e.g., RN 0.74 → Reanimated 3.10.x). Ensure newArchEnabled=true in gradle.properties and Podfile use_frameworks! :linkage => :static consistent. Recovery: Temporarily disable Fabric for the component: UIManager.setViewManagerConfigurationDescription('MyView', null) in JS.
Reanimated Worklet Errors
Symptom: WorkletError: "worklet" is not defined or ReanimatedError: [Reanimated] Mismatch. Cause: Import order (react-native-gesture-handler must be first), missing babel-plugin-reanimated, or Hermes/JSC mismatch. Fix:
// index.js or App.js — very first import
import 'react-native-gesture-handler';
Verify babel.config.js:
plugins: [
['react-native-reanimated/plugin', { /* options */ }],
],
Expected: No worklet errors; animations run on UI thread. Failure: Forgetting to clear Metro cache after babel config change.
Network & SSL Deep Dive
Device Debugging with adb reverse
# Metro bundler
adb reverse tcp:8081 tcp:8081
# Local API server
adb reverse tcp:3000 tcp:3000
# Verify
adb reverse --list
Charles / Proxyman SSL Pinning Bypass
- Install CA on device/emulator:
adb push charles-ssl-proxying-certificate.pem /sdcard/Download/then Settings → Security → Install from SD card. - Or use
network_security_config.xml(debug only):
<!-- android/app/src/debug/res/xml/network_security_config.xml -->
<network-security-config>
<base-config cleartextTrafficPermitted="true">
<trust-anchors>
<certificates src="system" />
<certificates src="user" />
</trust-anchors>
</base-config>
<debug-overrides>
<trust-anchors>
<certificates src="system" />
<certificates src="user" />
</trust-anchors>
</debug-overrides>
</network-security-config>
- Reference in
AndroidManifest.xml(debug variant only viaandroid:networkSecurityConfig="@xml/network_security_config").
iOS ATS Exceptions (Debug Only)
Use xcconfig per configuration:
// Debug.xcconfig
NSAppTransportSecurity = {
NSExceptionDomains = {
"localhost" = {
NSExceptionAllowsInsecureHTTPLoads = YES;
NSIncludesSubdomains = YES;
};
};
};
Release.xcconfig: No exceptions; enforce HTTPS.
Certificate Pinning Bypass (Testing Only)
Libraries: react-native-ssl-pinning (Android), TrustKit (iOS). Never ship pinning bypass in production. Use only for QA against staging with known certs.
Fetch vs Axios vs Ky Timeout Defaults
Recommendation: Wrap all network calls with a 10–15s timeout and retry logic (exponential backoff, max 3 retries).
fetch: No timeout by default; implementAbortControllerwithsetTimeout.axios: Default 0 (no timeout); settimeout: 10000in instance config.ky: Default 10s; configurable viatimeoutoption.
Billing & Payments (Expanded)
Google Play Billing (react-native-iap / react-native-purchases)
ITEM_UNAVAILABLE / SKU not found:
- Verify product ID exact match in Play Console (case-sensitive).
- Product must be Active, not "Inactive" or "Draft".
- Tester account must be in License Testers list (not just internal test track).
- Use internal test track for fastest propagation (minutes vs hours).
BillingClient not ready:
import { initConnection, getProducts, purchaseProduct } from 'react-native-iap';
await initConnection();
// Wait for ready
const products = await getProducts({ skus: ['premium_monthly'] });
Verification: adb logcat -s BillingClient:V IabHelper:V shows onBillingSetupFinished with OK.
RevenueCat / react-native-purchases
Common errors:
Purchases.configurecalled multiple times or beforeAppRegistry.- Entitlement computation mismatch:
customerInfo.entitlements.active['pro']vsall['pro']. logIn/logOutnot balanced; anonymous IDs leak across users.restorePurchasesfails on simulator (requires device).
Configuration pattern:
import Purchases from 'react-native-purchases';
Purchases.configure({
apiKey: Platform.select({ ios: 'appl_...', android: 'goog_...' }),
useAmazon: false,
observerMode: false, // true only if you handle purchases yourself
});
// Entitlement check
const customerInfo = await Purchases.getCustomerInfo();
const isPro = customerInfo.entitlements.active.pro !== undefined;
Server-side verification: Call RevenueCat REST API /v1/subscribers/{app_user_id} from your backend to validate entitlements before granting access.
Apple StoreKit 2 / App Store Connect
- Test with StoreKit Configuration File (
.storekit) in Xcode for local testing without network. Transaction.currentEntitlements(iOS 15+) for on-device verification.- Server-side receipt verification: POST
https://api.revenuecat.com/v1/subscribers/{id}or Apple/verifyReceiptendpoint (deprecated, use App Store Server API).
Performance & Profiling
| Tool | Purpose | Command / Setup |
|---|---|---|
react-native-performance (Sentry) | Vitals, slow frames, TTI | npm i @sentry/react-native + Sentry.init({ dsn, enablePerformance: true }) |
why-did-you-render | Unnecessary re-renders | npm i @welldone-software/why-did-you-render + patch React in App.js |
hermes-profile / chrome://tracing | CPU profiling | adb shell setprop debug.hermes.profiler 1 → load trace in Chrome |
metro-bundle-analyzer | Bundle size | npx metro-bundle-analyzer → open HTML report |
react-native-reanimated profiler | Worklet execution time | Reanimated.setProfilingEnabled(true) in dev |
Quick profile: adb shell setprop debug.hermes.profiler 1, reproduce scenario, adb shell setprop debug.hermes.profiler 0, pull trace: adb shell run-as com.myapp cat files/.hermes/*.trace | npx metro-symbolicate > profile.json, open chrome://tracing → Load.
CI/CD & Automation Signals
GitHub Actions Artifact Upload
- name: Upload Gradle scan
uses: actions/upload-artifact@v4
if: always()
with:
name: gradle-scan-${{ github.run_id }}
path: android/build/reports/scan/
- name: Upload Xcode result bundle
uses: actions/upload-artifact@v4
if: always()
with:
name: xcode-result-${{ github.run_id }}
path: build.xcresult
Gradle Scan Link
./gradlew assembleRelease --no-daemon --scan -PreactNativeArchitectures=arm64-v8a
# Output: "Publishing build scan... https://gradle.com/s/xxxx"
Xcode Result Bundle
xcodebuild -workspace MyApp.xcworkspace \
-scheme MyApp \
-configuration Release \
-destination generic/platform=iOS \
-resultBundlePath build.xcresult \
-quiet
# Parse with xcparse or xcresulttool
EAS Build Logs
eas build:list --platform=android --limit=5
eas build:view <build-id> --logs
Fastlane Scan/Gym Output
# Fastfile
lane :test do
scan(
workspace: "MyApp.xcworkspace",
scheme: "MyApp",
result_bundle: true,
output_directory: "fastlane/test_output"
)
end
Rollback & Recovery (Expanded)
Expo Updates Rollback
# List recent updates
npx expo-updates:list
# Rollback to previous
npx expo-updates:rollback
# Or publish a new update with previous bundle
eas update --branch production --message "Rollback to v1.2.3"
EAS Build Rebuild from Cache
eas build --profile production --platform android --clear-cache
# Or rebuild specific commit
eas build --profile production --platform android --commit <sha>
Git Bisect Automation
# Automated bisect for regression
git bisect start HEAD <good-tag>
git bisect run npm ci && npm test
git bisect reset
Native Module Rebuild Isolation
# Reinstall JS deps without native rebuild scripts
npm ci --ignore-scripts
# Then rebuild native only
cd android && ./gradlew clean assembleDebug
cd ../ios && pod install --repo-update
Realistic Technical Scenario
Scenario: Android release build crashes on startup with java.lang.UnsatisfiedLinkError: libhermes.so after upgrading RN 0.73→0.74 and adding react-native-reanimated 3.x.
1. Inventory Capture
npx react-native info
# Output shows: RN 0.74.2, Hermes enabled, AGP 8.2.2, JDK 21, Reanimated 3.10.0
2. Log Correlation
# logcat for native crash
adb logcat -s ReactNative:V System.err:V -v threadtime > crash.log
# Hermes crash dump
adb shell run-as com.myapp cat files/.hermes/*.log | npx metro-symbolicate > hermes-crash.txt
Key logcat lines:
E System.err: java.lang.UnsatisfiedLinkError: dlopen failed: library "libhermes.so" not found
E System.err: at com.facebook.soloader.SoLoader.loadLibrary(SoLoader.java:...)
E ReactNativeJS: Fatal: TurboModuleRegistry.getEnforcing(...): 'NativeReanimated' not found
3. Narrow Scope
- Disable Reanimated: Comment out
import 'react-native-reanimated';and Reanimated usage. - Rebuild:
cd android && ./gradlew clean assembleRelease --stacktrace. - Result: App launches → Reanimated is the trigger.
4. Root Cause & Fix
Cause: RN 0.74 requires react-native-reanimated 3.10+ with Fabric enabled. libhermes.so missing because hermes_enabled=true but newArchEnabled=false in gradle.properties — Hermes JSI bindings for TurboModules not generated. Also soLoader config in android/app/build.gradle missing jniLibs for Hermes.
Fix (android/gradle.properties):
hermes_enabled=true
newArchEnabled=true # Required for TurboModules/Fabric
Fix (android/app/build.gradle):
dependencies {
// Ensure Hermes JSI libs are packaged
implementation "com.facebook.react:hermes-engine:+"
}
Fix (ios/Podfile):
use_frameworks! :linkage => :static
# Ensure Reanimated pods use Fabric
pod 'React-RCTFabric', :path => '../node_modules/react-native/React/Fabric'
Run:
cd android && ./gradlew clean assembleRelease --no-daemon --scan -PreactNativeArchitectures=arm64-v8a
cd ../ios && pod install --repo-update
5. Verify
- Release APK installs and launches on device.
adb logcat -s ReactNative:VshowsTurboModuleRegistry.getEnforcing: NativeReanimatedsuccess.- Reanimated animations run on UI thread (no JS thread drops).
6. Rollback Plan
# Revert gradle.properties
git checkout HEAD -- android/gradle.properties
# Revert Reanimated version
npm install [email protected] # last version compatible with RN 0.73
# Rebuild
cd android && ./gradlew clean assembleRelease
Decision: If timeline critical, rollback RN to 0.73.3 and Reanimated to 3.6.2; schedule RN 0.74 migration with dedicated QA.
Conclusion
React Native troubleshooting is most effective when you stabilize the environment, gather high-signal logs, and make the smallest safe change. With a clear inventory, predictable debug and release settings, and a disciplined verify-and-rollback routine, most problems become straightforward to isolate and fix. Use the inventory commands to establish a baseline, the log commands to observe real behavior across Metro, logcat, OSLog, and Hermes crash dumps, and the example-driven fixes here to resolve common issues with Metro resolution, Android and iOS builds, REST networking, Google Play Billing, Stripe, and RevenueCat integrations. Keep changes small, verify locally on both platforms, and roll back quickly if needed. Over time, this approach shortens incident duration and makes outcomes reliable across your team.