Skip to content

Legacy and V1 engines

From React Native 0.83 onward there is not one Hermes, there are two.

Legacy Hermes Hermes V1 (Static Hermes)
HBC bytecode version 96 98
Introduced The original engine RN 0.83 (pinned alongside legacy)
Default engine up to RN 0.83 RN 0.84 onward
Still available removed in RN 0.87 yes
class syntax not parsed at all full support
Private fields, static blocks no yes
async arrow functions no yes
for await…of, WeakRef no yes

Argus targets V1 by default and treats legacy as the compatibility mode, mirroring where React Native itself is.

Static Hermes did not replace the interpreter

Section titled “Static Hermes did not replace the interpreter”

A common misreading: “Static Hermes” sounds like an ahead-of-time compiler replacing the VM. It is not. Static Hermes added an AOT native compiler alongside the bytecode interpreter.

The hermes VM CLI target still exists on every Static Hermes ref, builds on a plain host toolchain in roughly 95 seconds, and produces a self-contained ~4.8 MB binary. That is the binary Argus runs.

This matters for fidelity: React Native compiles your bundle to HBC bytecode at build time and interprets it on device. Same model Argus uses. A locally built hermesc at hermes-v250829098.0.16 emits bytecode byte-identical to the hermes-compiler npm package React Native 0.87 ships — which is the parity gate every published prebuilt must pass.

The gap between the engines is large in what they can parse and small in what they observe at runtime.

V1 adds class in every form, private fields, static blocks, async arrows, for await…of and WeakRef — none of which legacy can parse at all.

The runtime observables Argus depends on are unchanged across both:

  • Microtask ordering
  • Error stack shape
  • Number formatting
  • JSON key ordering
  • Regex semantics
  • Typed arrays

Intl is a build flag, not an engine difference. React Native builds with it on, so Argus builds with it on too.

The practical consequence: the Babel class-lowering step in the bundler exists purely to work around a legacy parser limitation. On V1 it is unnecessary — see The syntax envelope.

By the binary’s own HBC bytecode version, not by its release version string.

Release version is unreliable: a plain clone of any Hermes tag reports 1.0.0, because CMakeLists.txt declares project(Hermes VERSION 1.0.0) and defaults HERMES_RELEASE_VERSION to it. The real version has to be injected at configure time. That is why a stock legacy build can claim 0.12.0 for what is actually hermes-v0.17.0.

Bytecode version is baked into the VM’s format support and cannot drift. And a VM refuses foreign bytecode outright:

Wrong bytecode version. Expected 98 but got 96

Loud, immediate, unmistakable — never a silent degradation.

By default Argus uses whichever engine your project pins, preferring V1 when both are pinned. Override it explicitly:

Terminal window
argus --engine v1 "src/**/*.test.ts"
argus --engine legacy "src/**/*.test.ts"

Asking for an engine your project does not pin is a usage error, not a silent fallback:

--engine legacy is not available: react-native 0.87.0 pins only: v1.

When you name a binary yourself with --hermes or ARGUS_HERMES, Argus can no longer guarantee it matches your project’s engine. It checks anyway, and warns:

⚠ Engine mismatch: this project targets v1 (bytecode 98), but the provisioned binary is
legacy (bytecode 96).
binary: /opt/hermes/bin/hermes
Tests will run on that binary, so results reflect legacy, not the engine this project
ships.
Fix: drop --hermes/ARGUS_HERMES to provision the pinned engine, or pass --engine legacy
if this project pins legacy too.

A warning, never a hard failure. Running a deliberately mismatched binary is legitimate — bisecting an engine bug, testing a patched VM — and refusing would take that away. What is not legitimate is doing it by accident and never being told.

A binary that reports no bytecode version at all produces no warning: absence of evidence is not a mismatch, and warning on a guess would train you to ignore the warning.