Discussing how to generate source maps for Hermes bytecode, read the error stack traces that appear in production, and best practices for error handling and telemetry so every crash can be traced back to its original line of code.

Episode 9 wrapped up the story of Hermes beyond mobile: embedding via JSI, bytecode compilation with hermesc, and the reality that Hermes isn't a Node.js replacement. Now we return to the surface you touch most often in production: when the app errors, and you need to know what happened from the stack trace that comes in.
The roadmap for episode 10: generating source maps for Hermes bytecode, debugging error stack traces in production, then best practices for error handling and telemetry. After this episode, crash reports are no longer a puzzle you have to guess at.
A release JavaScript bundle is one large, minified file: variable names are shortened, lines are merged, and functions are called from offsets within the file. On top of that, Hermes compiles the bundle into .hbc bytecode executed by the interpreter. When an error occurs, all the interpreter can report is a position inside the bytecode — not the original line of code you wrote.
A source map is a file that maps positions in the output code back to lines and columns in the original source. It's the bridge that turns an error message like "TypeError at index.android.bundle:1:4419" into "TypeError at src/screens/Profile.js:23".
One important thing: source maps are only useful when sent to an observability tool, not stored on the device. If a source map gets installed with the app, anyone can read your code — and that punches a hole in the hardening layer we'll build in episode 13.
In React Native, the source map pipeline has two layers. First, Metro generates a source map from JavaScript. Second, hermesc refines it so it maps bytecode offsets to the original code. When building a release bundle, enable both. The easiest way to try it is from the project directory with npx react-native bundle --platform android --dev false:
npx react-native bundle \
--platform android \
--dev false \
--entry-file index.js \
--bundle-output index.android.bundle \
--sourcemap-output index.android.mapTo make sure hermesc also produces the bytecode mapping, add the -output-source-map flag to hermesFlags in build.gradle:
react {
hermesFlags = [
"-O",
"-output-source-map",
]
}The combination produces two artifacts: the bytecode bundle for the app, and the source map for telemetry. For embeddable workloads outside React Native, hermesc is called directly with the same flags:
hermesc -O -emit-binary -output-source-map -out worker.hbc worker.jsWhen a bug slips into production, telemetry receives something like this:
Error: Gagal mengirim pesanan
at submitOrder (index.android.bundle:1:4419)
at CheckoutScreen.<anonymous> (index.android.bundle:1:8877)
at onPress (index.android.bundle:1:12033)
at HermesInternal.jsFunctionCall (native)
at executeOnJSThread (native)The HermesInternal and executeOnJSThread frames are noise from inside the runtime — just ignore them. What matters are the top three frames: each contains a minified function name and a bytecode offset. Those offset numbers are what the source map must map to find the original location.
If your stack trace is empty or only contains (native), check whether the -output-source-map flag is really active in the release build. Without the bytecode mapping, tools like Sentry or Crashlytics can't rewrite that stack trace.
Info
Make sure the uploaded source map version matches the bytecode installed in the app exactly. If the build changes without uploading a new source map, the symbolication result will point the wrong way — worse than having no source map at all.
The process of translating a raw stack trace into original code lines is called symbolication. For Hermes, hermesc has a special mode that takes a source map and a stack trace, then outputs the translated version:
hermesc -symbolicate -output-source-map index.android.map < stack.txtThe more common pipeline in React Native: upload the source map to the telemetry service at release time (for example with sentry-cli and the upload-sourcemaps command), then let that service do automatic symbolication for every incoming stack trace. Note the upload order: the source map must be uploaded before the app ships to users.
Besides error stack traces, don't forget to capture context: app version, Hermes version, device model, and OS. That context is what distinguishes "many users are hitting an error" from "one device is broken".
Good error handling starts at a single entry point. In React Native, we can replace the global handler to catch uncaught errors before they become truly fatal:
const previousHandler = ErrorUtils.getGlobalHandler();
ErrorUtils.setGlobalHandler((error, isFatal) => {
Telemetry.captureException(error, {
extra: { isFatal },
tags: { build: "2026-08-03" },
});
previousHandler(error, isFatal);
});Next, a few habits that make telemetry genuinely useful:
try/catch helper, so the report format stays consistent.A clean source map, a symbolicated stack trace, and a consistent handler turn production debugging from a guessing game into a reading activity.
You now have the complete chain: source maps generated at build time, uploaded to telemetry, then used to translate every incoming Hermes stack trace. With a global error handler and good telemetry habits, every crash has a clear traceable path back to its original line of code.
The essentials to take home:
-output-source-map flag from hermesc.HermesInternal and (native) frames in a stack trace are runtime noise and can be ignored.hermesc -symbolicate or a telemetry service to translate production stack traces.In the next episode, episode 11, we tidy up the house: configuration management & build validation — how to store Hermes configuration so builds are reproducible, validate output in CI, and integrate everything with linting and the release pipeline. See you there!