How do you get a source map for a React Native release build on Android and on iOS, and which file do you use?
answer
- Android on, iOS off by default
- hermesFlags: -O, -output-source-map
- SOURCEMAP_FILE in the bundling build phase
- generated/sourcemaps, not intermediates
- pipe the trace into metro-symbolicate
basics
~10 sAndroid release builds write one by default because hermesFlags includes -output-source-map; use generated/sourcemaps/react/release/index.android.bundle.map. iOS writes none until you export SOURCEMAP_FILE in Xcode's Bundle React Native code and images build phase.
solid answer
~30 sOn Android the React Native Gradle plugin's `hermesFlags` default to `["-O", "-output-source-map"]`, so a release build already produces a composed map at `android/app/build/generated/sourcemaps/react/release/index.android.bundle.map`; the `*.packager.map` and `*.compiler.map` under `intermediates/` are inputs, not the file to use. On iOS source maps are off by default: I edit the **Bundle React Native code and images** build phase and export `SOURCEMAP_FILE` with an output path, and the script then has Metro and hermesc emit maps and composes them into that file. Then I symbolicate with `npx metro-symbolicate <map> < stacktrace.txt`, or pipe `adb logcat -d` into it, and archive the map with the build.
code
bash · 2 lines# iOS: added at the top of the "Bundle React Native code and images" build phase
export SOURCEMAP_FILE="$(pwd)/../main.jsbundle.map"go deeper
Remember that Android makes the map by default and iOS needs SOURCEMAP_FILE, and that metro-symbolicate uses it.
Explain the build pieces: Metro's packager map, hermesc's compiler map, the composed map, and where each platform writes the one you use.
Catch the traps in real projects: overridden hermesFlags, iOS projects that never set SOURCEMAP_FILE, the wrong intermediate map, and maps not archived per build.
Make map generation and storage a checked step of every release so readable crashes don't depend on one engineer's local setup.
## Why the platforms differ Both platforms bundle JavaScript for a release build with the same pieces: **Metro** writes the bundle (and, when asked, a *packager* source map), **hermesc** compiles it to bytecode (and, when asked, a *compiler* source map), and a script composes the two into the map you symbolicate with. The difference is the default: the **Android Gradle plugin asks for the map unless you stop it**, while the **iOS build script only asks when you set a variable**. | | Android | iOS | |---|---|---| | Default | On | Off | | Switch | `hermesFlags` includes `-output-source-map` (the default) | `SOURCEMAP_FILE` exported in the bundling build phase | | Map to use | `android/app/build/generated/sourcemaps/react/<variant>/index.android.bundle.map` | Whatever path you gave `SOURCEMAP_FILE` | | Intermediates | `intermediates/sourcemaps/react/<variant>/*.packager.map` and `*.compiler.map` are kept | Deleted after composition | ## Android The React Native Gradle plugin configures release bundling. Its `hermesFlags` property defaults to `["-O", "-output-source-map"]`, so a normal release build produces a map without any change. The docs show the explicit form in `android/app/build.gradle`: ```groovy react { hermesFlags = ["-O", "-output-source-map"] } ``` Points worth knowing: - The map you want is the **composed** one in `generated/sourcemaps/react/release/`. The build also leaves a packager map and a compiler map under `intermediates/`; the docs warn that several maps are generated and you should use the one in the documented location. - If you override `hermesFlags` and drop `-output-source-map`, the plugin skips composition and **no map appears** in `generated/sourcemaps`. - The build log confirms it: Metro prints a `Writing sourcemap output to:` line. ## iOS The Xcode build phase **"Bundle React Native code and images"** runs `react-native-xcode.sh`. It emits a source map only when `SOURCEMAP_FILE` is set: 1. Open the app target's build phases in Xcode and edit **Bundle React Native code and images**. 2. Above the other exports, add `export SOURCEMAP_FILE="$(pwd)/../main.jsbundle.map"`, or any path outside the app bundle. 3. Build or archive the Release configuration and look for `Writing sourcemap output to:` in the build log. When the variable is set, the script passes `--sourcemap-output` to Metro. With Hermes it also adds `-output-source-map` to the hermesc call, composes the two maps into `SOURCEMAP_FILE` with `compose-source-maps.js`, and deletes the two intermediate maps. Choose a path outside the app bundle and one your archive step knows about, since that is the only copy. Without the variable, an iOS release build produces **no map at all**, and a crash from it cannot be symbolicated later unless you can reproduce that exact build. ## Running metro-symbolicate With the map in hand, feed the stack trace on standard input: - from a saved file: `npx metro-symbolicate <map> < stacktrace.txt`; - straight from Android logs: `adb logcat -d | npx metro-symbolicate <map>`; - with no arguments, `npx metro-symbolicate` prints its usage. One trap from the docs: if `metro-symbolicate` **exits immediately with success**, it was reading from the terminal instead of a pipe or redirect, so it received no trace. ## Checking the map before you need it A map is only discovered to be missing or wrong when a crash needs it, which is the worst moment. Cheap checks at build time: 1. Confirm the file exists at the expected path and is not empty. 2. Open it and check that its `sources` list contains your own files, for example `src/screens/CheckInScreen.tsx`, not only `node_modules` paths. 3. Once per setup change, plant a JavaScript crash in a release build, capture the trace and confirm `metro-symbolicate` points at the planted line. The third check is the only one that proves the whole chain: generation, composition, archiving and matching. ## Where the map should live The map is written next to the build outputs, not packaged into the app, and it describes only the bundle it came from. Treat it as part of the release: - store it with the build that produced it, keyed by version, build number and commit; - make the release pipeline fail if the expected map is missing; - regenerate nothing after the fact; a rebuilt map from different inputs will not line up.
- A teammate set hermesFlags = ["-O"] in build.gradle to tweak Hermes. What happens to source maps?The Gradle plugin composes a map only when hermesFlags contains -output-source-map. Without it, hermesc writes no compiler map, composition is skipped, and nothing appears in generated/sourcemaps; only Metro's packager map sits in intermediates, which cannot resolve bytecode offsets. Keep -output-source-map in any custom flag list.
- Why keep the map out of the app bundle?Nothing on the device needs it: symbolication happens off-device, after a crash is collected. The map reveals file names and code structure, and it only matters alongside the exact build it came from, so it belongs in private release storage keyed by version and build number, not in the shipped app.
saying these in an interview costs you the question
- iOS release builds write a source map by default, just like Android.
- Use the index.android.bundle.packager.map file to symbolicate Hermes crashes.
- Set source maps on in metro.config.js and both platforms produce one.
- If metro-symbolicate exits cleanly, the trace had nothing to fix.
- You can regenerate a lost map later from any build of that version.