Skip to content

Fix: React Native Metro Bundler Failed to Start or Bundle

FixDevs · (Updated: )

Part of:  JavaScript & TypeScript Errors

Quick Answer

How to fix React Native Metro bundler errors — unable to resolve module, EMFILE too many open files, port already in use, transform cache errors, and Metro failing to start on iOS or Android.

The Error

When running npx react-native start or npx expo start, Metro fails with one of these errors:

error: Error: Unable to resolve module `./src/screens/Home` from `App.js`

Or Metro fails to start:

Error: EMFILE: too many open files, watch

Or a port conflict:

Another process is already using port 8081. Run the following command to find out which process:
  lsof -i :8081

Or a transform error:

error: bundling failed: Error: Unable to resolve module `crypto` from `node_modules/some-library/index.js`

Or after an npm install, bundles fail silently and the simulator shows a red error screen with “Unable to load script.”

Why This Happens

Metro is React Native’s JavaScript bundler — it resolves module imports, transforms code, and serves the bundle to the simulator or device. Metro bundler failures stem from several common sources:

  • Stale cache — Metro caches transform results. After dependency changes, the cache can reference modules that have moved or been removed.
  • Module resolution failure — the moduleNameMapper or Metro config does not know how to resolve a path alias, a native module, or a Node.js built-in.
  • File watcher limits — on Linux, the default inotify limit is too low for large React Native projects.
  • Port conflict — Metro defaults to port 8081. Another Metro instance or a different process already occupies it.
  • Node.js built-in modules in web-targeted libraries — some npm packages use Node.js built-ins (crypto, stream, fs) that do not exist in React Native’s runtime.
  • Corrupted node_modules — incomplete installs or version conflicts cause Metro to fail resolving modules it expects to find.

Two architectural details matter when triaging Metro errors. First, Metro is a long-running development server that holds state in memory and on disk: a transform cache keyed by file content and Babel config, a HasteMap of every file in the project, and a Watchman crawl of the working tree. A subtle change like a renamed file, a Babel plugin upgrade, or a package.json edit can desynchronize one of these pieces from reality, and the symptoms range from a missing module to a phantom older version of the file being bundled. Almost every “tried everything” Metro problem ends up resolved by clearing the cache plus restarting Watchman.

Second, Metro’s resolver mimics Node’s resolution algorithm with React Native-specific extensions. It tries platform-specific extensions (.ios.js, .android.js, .native.js) before generic ones, honors the react-native field in package.json ahead of main, and resolves node_modules differently in monorepos than in single-package projects. A library that works in a Node script may explode in Metro if it relies on module.exports = require('fs') or other built-ins; conversely, a library installed once in a sibling workspace may resolve in dev but not after metro-config upgrades. Read the failing import path twice before reaching for --reset-cache.

Platform and Environment Differences

React Native’s bundler is the same Metro on every platform, but the environment around it varies enough to produce platform-specific failure modes:

  • iOS vs Android Metro path differences. Metro serves bundles over HTTP at http://localhost:8081/index.bundle?platform=ios or ?platform=android. iOS simulators reach localhost natively; Android emulators map the host’s localhost to 10.0.2.2, and physical Android devices need adb reverse tcp:8081 tcp:8081. The same Metro instance can succeed on iOS and fail on Android purely because port reverse is missing.
  • Apple Silicon Macs and Hermes arm64. On M1/M2/M3 Macs, Hermes ships with arm64 binaries that run natively. Older project setups that install dependencies under Rosetta produce mixed arm64/x86_64 binaries, and the simulator can mismatch with the bundler. Run arch in your terminal to confirm you are not in Rosetta, and reinstall pods with arch -arm64 pod install if the simulator crashes immediately after Metro reports success.
  • Windows lacks iOS targets. The React Native CLI on Windows can only build Android. iOS builds and the matching pod install step require macOS and Xcode. Teams using Windows for development typically run Metro locally (Android) and rely on EAS Build or a Mac build agent for iOS. If react-native run-ios errors out with “no Xcode” on Windows, that is by design — switch to Android or use a remote macOS build.
  • Expo Go vs custom dev client. Expo Go is a prebuilt app that loads JavaScript bundles from Metro but cannot run native code you add yourself. The moment your project includes a native module not bundled into Expo Go, you must build a custom dev client (npx expo prebuild + eas build --profile development). Metro itself happily serves the bundle either way; the runtime error you see in Expo Go (“RNFoo not found”) is a native binding issue, not a Metro bug.
  • EAS Build cloud vs local. eas build runs on Expo’s hosted infrastructure with pinned macOS and Linux images. Metro behaviors that depend on host-specific caches (Watchman state, TMPDIR layout) won’t carry over. Reproduce a failing CI build locally with eas build --local to use the same image without uploading.
  • Hermes vs JSC engine choice. React Native 0.70+ defaults to the Hermes engine. Hermes parses and runs precompiled bytecode, so Metro’s output includes a Hermes bytecode bundle for release builds. Hermes is strict about modern JavaScript features and has its own debugger protocol; if your release crashes but dev works, suspect Hermes-specific transforms, not Metro. JSC remains an option (jsEngine: "jsc" in react-native.config.js or app.json) for projects that hit Hermes compatibility issues.
  • Linux file watcher limits. Linux uses inotify for file watches, capped at fs.inotify.max_user_watches (often 8192 by default). A React Native project with thousands of files in node_modules exhausts the cap immediately. macOS uses FSEvents via Watchman; Windows uses ReadDirectoryChangesW. The same project fails to start on Linux while running fine on macOS because of this single setting.

Fix 1: Clear the Metro Cache

The most common fix for sudden Metro failures after npm install or git pull:

# Clear Metro's transform cache
npx react-native start --reset-cache

# Or for Expo
npx expo start --clear

# Full reset — clears watchman, metro cache, and node_modules cache
watchman watch-del-all
rm -rf $TMPDIR/metro-*
rm -rf $TMPDIR/react-*

For a complete clean start:

# Stop Metro if running
# Clear all caches
watchman watch-del-all 2>/dev/null || true
rm -rf node_modules
rm -rf $TMPDIR/metro-*
rm -rf $TMPDIR/haste-map-*
npm install
npx react-native start --reset-cache

Pro Tip: Add a clean script to your package.json for faster troubleshooting:

{
  "scripts": {
    "clean": "watchman watch-del-all && rm -rf $TMPDIR/metro-* && rm -rf node_modules && npm install",
    "start:clean": "npm run clean && npx react-native start --reset-cache"
  }
}

Fix 2: Fix Unable to Resolve Module

When Metro cannot find a module you are importing:

Check the import path first:

// Wrong — relative path typo
import HomeScreen from './screens/home'; // file is HomeScreen.js, not home.js

// Correct
import HomeScreen from './screens/HomeScreen';

Metro is case-sensitive even on macOS (unlike the filesystem). A file named HomeScreen.js is not resolved by ./screens/homescreen.

Add path aliases via Metro config:

// metro.config.js
const { getDefaultConfig } = require('@react-native/metro-config');
const path = require('path');

const config = getDefaultConfig(__dirname);

config.resolver.alias = {
  '@components': path.resolve(__dirname, 'src/components'),
  '@screens': path.resolve(__dirname, 'src/screens'),
  '@utils': path.resolve(__dirname, 'src/utils'),
};

module.exports = config;

Verify the module exists in node_modules:

ls node_modules/some-library/
# If empty or missing, reinstall:
npm install some-library

Fix missing peer dependencies:

npx react-native doctor
# Or
npx expo-doctor

Fix 3: Fix Node.js Built-in Modules Not Found

Some npm packages use Node.js built-ins (crypto, stream, buffer, path) that do not exist in React Native’s JavaScript environment:

error: bundling failed: Error: Unable to resolve module `crypto`

Install polyfill packages:

npm install react-native-crypto react-native-randombytes
npm install stream-browserify readable-stream
npm install react-native-buffer

Configure Metro to use polyfills:

// metro.config.js
const { getDefaultConfig } = require('@react-native/metro-config');

const config = getDefaultConfig(__dirname);

config.resolver.extraNodeModules = {
  crypto: require.resolve('react-native-crypto'),
  stream: require.resolve('stream-browserify'),
  buffer: require.resolve('react-native-buffer'),
  path: require.resolve('path-browserify'),
  os: require.resolve('react-native-os'),
};

module.exports = config;

Alternative — find a React Native compatible library:

Instead of polyfilling crypto, use a React Native native module:

# Use react-native-quick-crypto instead of the crypto polyfill
npm install react-native-quick-crypto
npx pod-install  # iOS

Fix 4: Fix EMFILE Too Many Open Files

On Linux and macOS, the file watcher limit is often too low for React Native projects with thousands of files:

Increase the inotify limit on Linux:

# Check the current limit
cat /proc/sys/fs/inotify/max_user_watches

# Increase it temporarily
sudo sysctl fs.inotify.max_user_watches=524288

# Make it permanent
echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf
sudo sysctl -p

On macOS — increase file descriptor limit:

# Check current limit
ulimit -n

# Increase for current session
ulimit -n 65536

# For permanent fix, add to ~/.zshrc or ~/.bash_profile:
echo "ulimit -n 65536" >> ~/.zshrc

Install and use Watchman (recommended):

Metro uses Watchman for file watching on macOS when available — it is more efficient than the native file watcher:

brew install watchman
watchman --version

# Reset Watchman state if it was previously broken
watchman watch-del-all

Fix 5: Fix Port 8081 Already in Use

Find and kill the process using port 8081:

# Find the process
lsof -i :8081

# Kill it
kill -9 $(lsof -t -i:8081)

# On Windows
netstat -ano | findstr :8081
taskkill /PID <PID> /F

Or run Metro on a different port:

npx react-native start --port 8082

# Then in a new terminal, run the app with the custom port:
npx react-native run-ios --port 8082
npx react-native run-android --port 8082

# For Expo:
npx expo start --port 8082

Update the app to use the new port (Android):

If you change the Metro port, Android needs to know:

adb reverse tcp:8082 tcp:8082

Fix 6: Fix Metro Transform and Babel Errors

Error: SyntaxError or Transform failed:

These usually indicate a Babel configuration issue or a file with syntax Metro cannot parse:

# Check your Babel config
cat babel.config.js
// babel.config.js — standard React Native config
module.exports = {
  presets: ['module:@react-native/babel-preset'],
  plugins: [
    // Optional: support path aliases (needs babel-plugin-module-resolver)
    [
      'module-resolver',
      {
        root: ['./src'],
        extensions: ['.ios.js', '.android.js', '.js', '.ts', '.tsx', '.json'],
        alias: {
          '@components': './src/components',
          '@screens': './src/screens',
        },
      },
    ],
  ],
};

Fix experimentalDecorators or TypeScript syntax errors:

# Install TypeScript support
npm install --save-dev @babel/plugin-proposal-decorators babel-plugin-parameter-decorator
// babel.config.js
module.exports = {
  presets: ['module:@react-native/babel-preset'],
  plugins: [
    ['@babel/plugin-proposal-decorators', { legacy: true }],
  ],
};

Clear the Babel cache after changing babel.config.js:

npx react-native start --reset-cache

Fix 7: Fix Metro on a Fresh Setup or CI Environment

When setting up a new environment or running in CI:

# Complete setup sequence
git clone https://github.com/your-org/your-app.git
cd your-app

# Install JS dependencies
npm ci

# iOS — install CocoaPods dependencies
cd ios && pod install && cd ..

# Start Metro (in one terminal)
npx react-native start

# Run on device (in another terminal)
npx react-native run-ios
npx react-native run-android

Common CI issues:

# CI may not have Watchman — disable it in Metro config
// metro.config.js
config.watchman = false; // Fall back to native file watching in CI

Android — ensure ADB is set up:

# Check connected devices
adb devices

# If no devices found for a running emulator:
adb kill-server && adb start-server

iOS — fix code signing for simulators:

# Simulators do not need code signing — check scheme settings
open ios/YourApp.xcworkspace
# Product → Scheme → Edit Scheme → Run → Build Configuration → Debug

Still Not Working?

Check the Metro output for the root error. Metro often shows a long stack trace — scroll to the top to find the original error message, which is more useful than the bottom of the stack.

Verify your Node.js version. React Native has specific Node.js version requirements. Check the project’s .nvmrc or the React Native version’s release notes:

node --version
# Use nvm to switch versions
nvm use 20
nvm install 20

Check for symlink issues. If you use npm link or workspaces, Metro may fail to resolve symlinked packages:

// metro.config.js — allow following symlinks
config.resolver.unstable_enableSymlinks = true;

Verify Xcode Command Line Tools on macOS:

xcode-select --install
sudo xcode-select --switch /Applications/Xcode.app

Check the device logs for additional error context:

# iOS simulator logs
xcrun simctl spawn booted log stream --level debug | grep -i "react native"

# Android device logs
adb logcat *:E | grep -i "react\|metro\|javascript"

Audit your monorepo Metro config. Workspaces (Yarn, pnpm, npm) place dependencies in a hoisted root node_modules, and Metro must be told to watch the additional folders. Set watchFolders to include the workspace root and nodeModulesPaths to include both the package’s and the root’s node_modules. Missing this leads to “Unable to resolve module” errors for packages that visibly exist on disk.

Confirm the Hermes engine version matches the React Native version. Hermes is bundled with React Native and pinned per release. Manually upgrading hermes-engine alone produces bytecode that the runtime cannot load. If you see Magic number mismatch or Hermes bundle version errors, downgrade to the pinned version or upgrade React Native cleanly.

Switch engines temporarily to isolate the failure. Flip jsEngine between Hermes and JSC in app.json (Expo) or the iOS/Android native config (bare RN). If the same Metro bundle works on one engine but not the other, the issue is engine-specific transforms or Hermes precompilation — not Metro itself.

Check that no antivirus is locking .cache/metro or node_modules. On Windows, Defender and corporate AV products can hold file handles during scans, causing Metro to crash with EBUSY or EPERM mid-bundle. Add the project directory to the exclusion list.

For related React and JavaScript bundling issues, see Fix: webpack Module Not Found and Fix: Cannot Use Import Statement Outside a Module. For Expo-specific startup failures, see Fix: Expo Not Working. For Android-native build failures that surface after Metro succeeds, see Fix: React Native Android Build Failed.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles