expo/expo · error · Error

process.exit(${code}) called in Hermes runtime

Error message

process.exit(${code}) called in Hermes runtime

What it means

Thrown by the process polyfill's exit() method in globals.js. Since the device-transformer runs inside the Expo Go app's Hermes/JSC runtime (not a Node process), calling process.exit would crash the app. The polyfill deliberately throws to make any accidental exit call loud and diagnosable rather than silently killing the runtime.

Source

Thrown at apps/expo-go/tools/device-transformer/globals.js:294

    const proc = {
      env: {},
      argv: ['jsc', 'device-transformer'],
      execArgv: [],
      platform: 'darwin',
      arch: 'arm64',
      version: 'v22.14.0',
      versions: { node: '22.14.0', v8: '12.4' },
      pid: 1,
      title: 'jsc',
      browser: false,
      exitCode: 0,
      cwd: () => '/project',
      chdir: () => {
        throw new Error('process.chdir not supported');
      },
      nextTick: (fn, ...args) => g.queueMicrotask(() => fn(...args)),
      exit: (code) => {
        throw new Error('process.exit(' + code + ') called in Hermes runtime');
      },
      hrtime: Object.assign(
        (prev) => {
          const ms = Date.now();
          const s = Math.floor(ms / 1000),
            ns = (ms % 1000) * 1e6;
          if (prev) {
            let ds = s - prev[0],
              dns = ns - prev[1];
            if (dns < 0) {
              ds--;
              dns += 1e9;
            }
            return [ds, dns];
          }
          return [s, ns];
        },
        { bigint: () => BigInt(Date.now()) * 1000000n }

View on GitHub (pinned to b09195aac2)

Solutions

  1. Trace the stack to find which module calls process.exit and what condition triggers it — the error path likely indicates a real problem (missing config, version mismatch).
  2. Fix the underlying condition that causes the exit call.
  3. If the exit is on a dead code path, stub the module via an esbuild onLoad handler.

Example fix

// before — throws, potentially masking the real issue
exit: (code) => {
  throw new Error('process.exit(' + code + ') called in Hermes runtime');
},

// after — log the stack before throwing for easier diagnosis
exit: (code) => {
  console.error('process.exit(' + code + ') called from:', new Error().stack);
  throw new Error('process.exit(' + code + ') called in Hermes runtime');
},
Defensive patterns

Strategy: try-catch

Validate before calling

// Scan bundled source for process.exit usage
function scanForProcessExit(sourceFiles) {
  const offenders = [];
  for (const { path, content } of sourceFiles) {
    if (/process\.exit\s*\(/.test(content)) {
      offenders.push(path);
    }
  }
  return offenders;
}

Try / catch

// Wrap transform calls to catch process.exit throws
try {
  transformModule(source, filename, moduleId, depIds);
} catch (err) {
  if (err.message.includes('process.exit(')) {
    // The real error is whatever caused the exit call — log full stack
    console.error('Underlying error caused process.exit:', err.stack);
  }
  throw err;
}

Prevention

When it happens

Trigger: Any bundled module calls process.exit(code) during the on-device transform. The polyfill constructs an error with the exit code in the message.

Common situations: A babel plugin, metro utility, or dependency calls process.exit on an error path (e.g., version mismatch, missing config); a dependency's CLI bootstrap code calls exit during module initialization.

Related errors


AI-assisted analysis of expo/expo@b09195aac2 (2026-08-12). Data as JSON: /api/errors/5d1d6f13fcb6f566. Report an issue: GitHub.