skip to content

Source Maps & Symbolication

Release stack traces point at minified bundle offsets or Hermes bytecode until source maps translate them back to source lines. Interviewers ask why a crash report is unreadable and how to fix it.

part ofReact Nativeoverview, primer and where to startread it →
on this pageshow

explore

questions

4

In a React Native release build, why does a JavaScript crash stack show frames like p@1:132161 instead of your file and line?

level: juniorimportance: must knowfreq 46%

answer

  1. what actually runs in release?
  2. one Metro bundle, then Hermes bytecode
  3. name plus bytecode offset
  4. source map from the same build
  5. metro-symbolicate translates

basics

~20 s

A release build runs one Metro bundle compiled to Hermes bytecode, so frames give a function name and a bytecode offset, not your files. Symbolication maps them back with the source map from that exact build, for example using metro-symbolicate.

solid answer

~40 s

In release, Metro packs all modules into a single bundle and Hermes compiles it to bytecode, so a JavaScript error is reported against that bundle: a function name plus a bytecode offset like `p@1:132161`, with no file or line. In a debug build the bundle comes from Metro, which can serve its source map, which is why LogBox and DevTools show real locations. To read a release crash you **symbolicate** it: take the source map generated for that build and run the trace through `metro-symbolicate`, which prints file, line and function name. The map must come from the exact commit that shipped, because small code changes move the offsets. Android writes the map by default; iOS needs `SOURCEMAP_FILE` set. Native frames need their own files, a dSYM on iOS and R8's `mapping.txt` on Android.

go deeper

for a junior

Recall that release code is one bundle compiled to Hermes bytecode, that a source map translates positions back, and that the tool is metro-symbolicate.

for a middle

Explain why debug builds show real locations and release builds don't, what a source map records, and why only the exact build's map works.

for a senior

Show that you keep maps per shipped build, know the iOS default is off, and separate JavaScript frames from dSYM and mapping.txt frames in a mixed crash.

for a principal

Treat symbol files as release artifacts the team owns, so any crash from any shipped build can be read without guesswork.

## What the frame is telling you A release build of a React Native app does not run your source files. At build time **Metro** combines every module of the app and its dependencies into **one bundle**, and with **Hermes** (the default engine) that bundle is then compiled to **bytecode**. A JavaScript error in that build is reported against the thing that actually ran. The React Native docs show an Android example: ```text com.facebook.react.common.JavascriptException: Failed, js engine: hermes, stack: p@1:132161 p@1:132084 f@1:131854 anonymous@1:131119 ``` Each frame is a **function name** followed by a **position in the compiled bundle**, here a bytecode offset such as `132161`. None of it names `CheckInScreen.tsx` or a line number, because at runtime there is no such file; there is only the bundle. The names themselves can be short or `anonymous`, so they rarely identify the code either. ## Why debug builds look fine In a debug build the app loads its bundle from the running Metro dev server, which can also serve the matching source map. That is why LogBox and React Native DevTools show your own files and lines during development. A release build embeds the bundle in the app, and nothing on the phone knows where your source files were. The information needed to translate positions back exists only if the build produced a source map and someone kept it. ## What a source map is A **source map** is a JSON file, produced alongside the bundle, that records how positions in the generated output correspond to positions in the original files. In outline it holds: - the list of **original source files**; - the list of original **identifier names**; - an encoded table of **mappings** from generated positions to source file, line, column and name. For a Hermes build the useful map is the one that links **bytecode positions** all the way back to your source, which the build creates by combining Metro's map with the Hermes compiler's. ## Symbolication **Symbolication** is the translation of those raw frames into readable ones such as `CheckInScreen.tsx:54:submitBoardingPass`. The steps are: 1. The release build generates a source map. Android does this by default; iOS needs it switched on. 2. You keep that exact file, tied to the exact build that shipped. 3. When a crash arrives, you feed its stack trace and the map to `metro-symbolicate`, for example `npx metro-symbolicate index.android.bundle.map < stacktrace.txt`. 4. The tool prints the same trace with file, line and function names restored. The React Native docs put the key constraint plainly: the map must come from the **exact commit** of the crashing app, because small source changes cause large differences in offsets. ## JavaScript frames are only one of three kinds A React Native crash can contain frames from three worlds, and each needs its own symbol file: | Frames from | Look like | Symbol file | Produced by | |---|---|---|---| | Your JavaScript on Hermes | `p@1:132161` | Source map | Metro plus the Hermes compiler | | iOS native code | Addresses in the app binary | dSYM | Xcode, per build | | Android Java/Kotlin with R8 on | Renamed classes such as `a.b.c` | `mapping.txt` | R8, per build | This question is about the first row; the other two matter when the crash starts or passes through native code. ## What symbolication does and does not give you - It restores **file, line, column and function name** for each JavaScript frame. - It runs **off the device**, on a developer machine or wherever crashes are processed; the phone never needs the map. - It does **not** recover variable values or app state at the time of the crash; for that you need logs or a reproduction. - It does **not** touch native frames; those need the dSYM or `mapping.txt` from the same build. - It is only as good as the match between map and build: a wrong map still prints confident, readable, wrong locations. ## Mistakes interviewers listen for - Saying release builds strip line numbers "for security"; the cause is bundling and bytecode compilation. - Assuming a map from a later commit will do. - Assuming every platform writes a map by default; iOS does not. - Expecting a source map to fix native frames.

  • Why don't you see this problem while developing?
    A debug build loads its bundle from the Metro dev server, which can also serve the matching source map, so LogBox and React Native DevTools resolve positions to your files and lines on the fly. A release build embeds the compiled bundle and has no dev server, so the translation has to happen afterwards, with a map you saved at build time.
  • Can a source map from a newer commit symbolicate an older crash if only one file changed?
    No. Every module is concatenated into one bundle, so a change in one file shifts the positions of everything after it, and the Hermes compiler's output shifts with it. The React Native docs require the map from the exact commit of the crashing app; a near-miss map produces confident but wrong file and line numbers.

A source map is the seating chart for a concert hall: a ticket saying row 1, seat 132161 is useless on its own, but the chart printed for that night's layout tells you which person sat there. Bring the chart from a different night and you will confidently name the wrong person.

saying these in an interview costs you the question

  • Release builds hide line numbers on purpose for security.
  • 132161 is the line number in the file that threw.
  • Any recent source map will do if the app version matches.
  • Both Android and iOS write a source map by default.
  • A JavaScript source map also fixes native iOS and Android frames.
open as a page

How do you get a source map for a React Native release build on Android and on iOS, and which file do you use?

level: middleimportance: should knowfreq 36%

basics

~10 s

Android 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.

open as a page

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?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Produce 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.

open as a page

In a React Native Hermes release build, why are two source maps composed into one, and what breaks if you skip it?

level: seniorimportance: nice to knowfreq 15%

basics

~20 s

Metro's packager map links the bundle to your sources; hermesc's compiler map links bytecode to the bundle. Hermes crash frames are bytecode positions, so compose-source-maps.js chains the two; without that, frames resolve wrongly or not at all.

open as a page