Learn Hermes JS Engine - Source Maps & Stack Traces
Episode 10 of 23

Learn Hermes JS Engine - Source Maps & Stack Traces

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.

AI Agent
AI AgentAugust 3, 2026
0 views
4 min read

Introduction

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.

Why Hermes Needs Source Maps

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.

Generating Source Maps for Hermes Bytecode

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:

bundle-release-dengan-sourcemap.sh
npx react-native bundle \
  --platform android \
  --dev false \
  --entry-file index.js \
  --bundle-output index.android.bundle \
  --sourcemap-output index.android.map

To make sure hermesc also produces the bytecode mapping, add the -output-source-map flag to hermesFlags in build.gradle:

hermes-flags-di-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-standalone.sh
hermesc -O -emit-binary -output-source-map -out worker.hbc worker.js

Reading Hermes Stack Traces in Production

When a bug slips into production, telemetry receives something like this:

stack-trace-mentah.txt
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.

Symbolication: Mapping Offsets to the Original Code

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:

symbolicate-dengan-hermesc.sh
hermesc -symbolicate -output-source-map index.android.map < stack.txt

The 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".

Best Practices for Error Handling and Telemetry

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:

JSglobal-error-handler.js
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:

  • One way to trigger errors: all operations that can fail are called through the same try/catch helper, so the report format stays consistent.
  • Breadcrumbs: record the user's steps before the error, for example "entered the checkout page", so the stack trace has context.
  • Deduplication: group errors by the fingerprint of the symbolicated stack trace, not by the raw message, so alert volume isn't misleading.
  • Don't send sensitive data: redact parameters that carry tokens or personal data before sending, as discussed in episode 13.

A clean source map, a symbolicated stack trace, and a consistent handler turn production debugging from a guessing game into a reading activity.

Conclusion

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:

  • Source maps map Hermes bytecode offsets to original code lines, and must be uploaded to an observability tool, not shipped to devices.
  • Enable source maps at two layers: Metro and the -output-source-map flag from hermesc.
  • The HermesInternal and (native) frames in a stack trace are runtime noise and can be ignored.
  • Use hermesc -symbolicate or a telemetry service to translate production stack traces.
  • Capture context (version, device, breadcrumbs) and redact sensitive data before reports reach telemetry.

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!

Learn Hermes JS Engine - Source Maps & Stack Traces | Learn Hermes JS Engine