A React Native flight-check-in app's release crashes show unreadable JavaScript frames and obfuscated Java frames. How do you make every future crash readable?
answer
- three layers, three symbol files
- iOS needs SOURCEMAP_FILE
- R8 mapping.txt, iOS dSYM
- same commit, same build number
- OTA bundles need their own maps
basics
~20 sProduce and keep every build's symbol files: the composed Hermes source map (iOS needs SOURCEMAP_FILE), R8's mapping.txt for Java frames and the dSYM for iOS native frames, keyed to that exact build, including over-the-air bundles, then verify with a planted crash.
solid answer
~40 sI'd treat it as a release discipline. JavaScript frames need the composed Hermes source map: Android writes it by default to `generated/sourcemaps/react/release/`, iOS only when `SOURCEMAP_FILE` is exported in the bundling build phase. Obfuscated Java frames mean R8 is on, so each build's `mapping.txt` must be kept, and iOS native frames need that build's dSYM. All of them are archived by the release job, keyed by version, build number and commit, and uploaded wherever crashes are collected, because the docs require the map from the exact commit; a near-miss map gives plausible but wrong lines. Over-the-air updates ship a new bundle, so they need their own maps. Finally I'd plant a crash in a release build to prove it arrives readable.
code
bash · 5 lines# Symbolicate the JavaScript part of an Android release crash from the device log
adb logcat -d | npx metro-symbolicate android/app/build/generated/sourcemaps/react/release/index.android.bundle.map
# Or from a saved trace
npx metro-symbolicate android/app/build/generated/sourcemaps/react/release/index.android.bundle.map < crash.txtgo deeper
Recall that JavaScript frames need a source map, Java frames R8's mapping.txt and iOS native frames a dSYM.
Explain why each file must come from the exact build, and which platform defaults leave a gap, starting with iOS's missing SOURCEMAP_FILE.
Design the release job so no build ships without archived, uploaded symbol files, cover over-the-air bundles, and prove it end to end with a planted crash.
Own crash readability as a release requirement with checks, so triage speed doesn't depend on who happened to build the app.
## The situation A flight-check-in app has just shipped. Crash reports arrive with two kinds of noise: JavaScript frames like `anonymous@1:131119`, and Java frames with class names such as `a.b.c`. Nobody can tell whether the crash is in the boarding-pass screen or in a native module. The fix is a **release discipline**, not a one-off command: every shipped build must produce and keep the right symbol files, and a crash must be matched to the build it came from. ## Three kinds of frames, three symbol files A React Native crash can mix frames from three layers, and each is translated by a different file: | Frames | Why unreadable | Symbol file | When it exists | |---|---|---|---| | JavaScript on Hermes | One bundle, compiled to bytecode | Composed source map | Android by default; iOS only with `SOURCEMAP_FILE` | | Android Java/Kotlin | R8 renamed classes and methods | R8's `mapping.txt` | Whenever shrinking is on for the release build | | iOS native | Raw addresses in the binary | dSYM | Produced by Xcode for each build | The unreadable Java frames most likely mean code shrinking is on: the template's `enableProguardInReleaseBuilds` flag was set to true, and R8 renamed classes and methods in that build, so its mapping file must be kept for every build. The unreadable JavaScript frames mean either no map was produced, typically on iOS, or it was not kept. ## Make every build produce its files 1. **Android JavaScript**: leave `hermesFlags` including `-output-source-map` (the default) and take the composed map from `android/app/build/generated/sourcemaps/react/release/index.android.bundle.map`. 2. **iOS JavaScript**: export `SOURCEMAP_FILE` in the **Bundle React Native code and images** build phase; otherwise no map exists. 3. **Android native**: keep R8's `mapping.txt` from the same build whenever shrinking is enabled. 4. **iOS native**: keep the dSYM that Xcode produces with the archive. ## Keep them tied to the exact build The React Native docs are blunt: the source map must correspond to the **exact commit** of the crashing app, because small changes cause large differences in offsets. The same holds for `mapping.txt` and dSYMs, which describe one specific binary. In practice: - store all symbol files as artifacts of the release job, keyed by **version, build number and commit**; - upload them wherever crashes are collected, in the same job that builds the binary, so no build ships without them; - fail the release if an expected file is missing, since discovering it later is too late; - remember **over-the-air updates**: an update ships a new JavaScript bundle to an existing binary, so a crash in it needs the update's own map, not the binary's. Expo's docs note that `eas update` and `npx expo export` generate Hermes bytecode bundles and their source maps. ## Reading a crash by hand When you need to read one now, for a JavaScript frame on Android: ```bash adb logcat -d | npx metro-symbolicate android/app/build/generated/sourcemaps/react/release/index.android.bundle.map ``` or save the trace to a file and redirect it in. Check the top frame against the code you expect; a plausible frame in unrelated code is the typical sign of a mismatched map. ## Crashes that already shipped without files For builds already in users' hands without a kept map there is no clean fix. You can try rebuilding the same commit with the same toolchain and dependencies, then verify the result by symbolicating a frame whose location you can confirm independently. If it does not line up, treat its output as unreliable. The lasting fix is making sure the next build cannot ship without its files. ## Triage once the files exist With symbol files in place, a crash report is read in layers: - **Where did it start?** A native frame at the top with JavaScript frames below usually means a native module threw in response to a JavaScript call. - **Which build?** Match the report's version and build number, and for over-the-air updates the update identifier, to the stored files before symbolicating anything. - **Which screen?** The symbolicated JavaScript frames name the component and handler, for example the boarding-pass submit handler. - **Is it new?** Compare against the previous build's crashes, which is only possible if both builds kept their files. ## Proving the setup works Before trusting the pipeline, plant a crash in a release build (for example behind a hidden debug gesture), let it report, and confirm it arrives readable on both platforms. It is the only test that exercises map generation, archiving and matching together.
- A crash comes from an over-the-air update rather than the store build. Which source map do you use?The update's own. An over-the-air update replaces the JavaScript bundle while the native binary stays the same, so the bytecode offsets belong to the update. Expo's docs note that eas update and npx expo export generate the bytecode bundles and their source maps; keep those per update, alongside the binary's own map.
- Why does a mismatched map do more harm than a missing one?A missing map leaves frames obviously unreadable, so nobody trusts them. A map from a nearby commit still produces file names, lines and functions, just the wrong ones, and the team investigates innocent code. That's why maps are keyed to the exact build and checked with a planted crash.
- Does a JavaScript source map help with the obfuscated Java frames?No. Those frames come from Android code that R8 renamed when shrinking was on, and only the mapping.txt from that same build reverses the renaming. Source maps describe the JavaScript bundle only; iOS native frames likewise need the build's dSYM.
saying these in an interview costs you the question
- One source map covers JavaScript, Java and iOS native frames.
- The map from the current main branch is close enough.
- iOS writes its source map by default, so nothing is needed there.
- An OTA update can reuse the store build's source map.
- Symbol files can be regenerated reliably whenever a crash needs them.