Fixing Wrong Line Numbers in React Native Crash Reports Caused by Missing or Mismatched Hermes Source Maps
A production crash report from a release build points to a line number that, when opened in the source file, shows completely unrelated code — a comment, an import statement, or nothing at all. The crash is real and the stack trace isn't lying; it's correctly reporting a line in the compiled Hermes bytecode, which only means something useful once it's run back through the exact source map generated for that specific release build.
The Problem
A crash analytics tool reports a JavaScript exception from a production React Native release, complete with a file name and line number in the stack trace. Opening that exact file at that exact line shows code that has nothing to do with the reported error — sometimes a blank line, an unrelated function, or code from a completely different feature. The crash itself is real and reproducible; only the location information attached to it is wrong, which makes the report effectively useless for finding the actual fault.
Why It Happens
Hermes compiles JavaScript to its own bytecode, and a stack trace from a release build reports positions in that bytecode, not in the original source
Unlike JSC, which could map a crash more directly back to source in some cases, Hermes always compiles the app's JavaScript bundle down to Hermes bytecode ahead of time for release builds. A crash that occurs at runtime is reported in terms of the bytecode's own internal line and column numbering — numbers that have no inherent relationship to the original .js/.ts source line numbers unless something explicitly translates between the two.
The source map that performs that translation has to be the exact one generated for that exact build — any mismatch produces confidently wrong results
A Hermes source map is a precise mapping generated at build time between bytecode positions and original source positions, specific to the exact bundle that was compiled. Using a source map from a different build — a newer commit, a different platform, or even the same commit built with different bundler settings — doesn't fail loudly; it simply maps to whatever line happens to exist at that bytecode offset in the mismatched source map's version of the file, producing a location that looks plausible but is simply unrelated to the real crash site.
A composed source map step is easy to skip or get the order wrong on, since Metro and Hermes each produce their own intermediate map
Metro's bundler produces a source map translating the final bundle back to original per-file source, and Hermes's bytecode compiler produces a second map translating its bytecode back to that bundle. Getting a crash back to real source requires composing both maps in the correct order; a build pipeline that only saves one of the two, or applies them in the wrong sequence, produces a translation that resolves to a real-looking line number in a real file, but one that has no relation to where the crash actually occurred.
The Fix
1. Confirm Hermes composed source map generation is actually enabled for release builds
# android/gradle.properties
hermesEnabled=true
# android/app/build.gradle (or via the React Native Gradle plugin config)
project.ext.react = [
bundleCommand: "bundle",
hermesCommand: "../../node_modules/react-native/sdks/hermesc/%OS-BIN%/hermesc",
composeSourceMaps: true,
]
The composed map — the single file that correctly chains Metro's bundle map through Hermes's bytecode map back to the original source — only gets produced when this composition step is explicitly enabled; some custom build pipelines disable it unintentionally while optimizing build times, silently reverting to the Metro-only map that doesn't account for Hermes's bytecode transformation at all.
2. Archive the exact composed source map for every release build, keyed to that build's version and build number
# Typical output location after a release build with sourcemap generation enabled
android/app/build/generated/sourcemaps/react/release/index.android.bundle.map
ios/main.jsbundle.map
# Rename and store per-build, e.g.:
cp index.android.bundle.map "symbols/app-1.4.2-build-57-android.map"
Because the mapping is only valid for the exact bundle it was generated alongside, the map for build 57 cannot symbolicate a crash from build 58, even if the two builds are nearly identical — every release needs its own map stored and retrievable by exact version and build number before the crash reporting tool needs it.
3. Symbolicate using the official react-native symbolicate tooling rather than a generic source-map library
npx react-native-source-map-generator symbolicate --sourcemap-path symbols/app-1.4.2-build-57-android.map --stack-trace-file crash-stack.txt
Hermes bytecode source maps use a format with Hermes-specific extensions that a generic JavaScript source-map consumer doesn't always interpret correctly; React Native's own symbolication tooling (or a crash-reporting SDK's equivalent, when it explicitly documents Hermes support) handles these extensions correctly, where ad hoc source-map parsing can silently produce an incorrect but plausible-looking translation instead of an outright error.
4. Verify the fix by deliberately crashing a release build and confirming the reported line matches reality
// Temporarily added to a release-mode-only code path, removed after verifying
if (__DEV__ === false) {
throw new Error("Symbolication verification test crash");
}
Rather than trusting the pipeline is correct, triggering one intentional, uniquely-identifiable crash in an actual release build and confirming the symbolicated report points exactly to that throw statement's line gives a concrete, falsifiable check that the whole map-generation-and-storage pipeline is actually wired correctly end to end, catching a broken step before it causes a real crash to be misattributed.
Why This Works
A wrong line number in a Hermes crash report is never a sign the crash itself is fake or the reporting tool is broken — it's a symptom of the translation between bytecode position and source position using the wrong map, a missing composed map, or tooling that doesn't understand Hermes's map format. Enabling composed source map generation, archiving the exact per-build map, and symbolicating with Hermes-aware tooling closes each of those gaps, and a deliberate test crash confirms the full chain actually resolves correctly rather than assuming it does.
Conclusion
A production crash report pointing at the wrong line is a source-map mismatch, not a corrupted or unreliable crash. Enabling composed source maps for release builds, storing the exact map per build version, and symbolicating with tooling that understands Hermes's bytecode map format turns a seemingly random, unusable line number into an accurate pointer to the actual faulty line.
