In a React Native release build, why does a JavaScript crash stack show frames like p@1:132161 instead of your file and line?
answer
- what actually runs in release?
- one Metro bundle, then Hermes bytecode
- name plus bytecode offset
- source map from the same build
- metro-symbolicate translates
basics
~20 sA 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 sIn 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
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.
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.
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.
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.