LEARN · DEBUGGING GUIDE

Metro Bundler 'Unable to Resolve Module' — Real Debugging & Fix Strategy

Metro fails to resolve a module because it can't find the file in its resolution graph. This is almost never a missing package—it's a cache, alias, or symlink issue.

IntermediateBuild tools6 min read

What this usually means

Metro has its own module resolution algorithm that differs from Node.js. It uses a blacklist, a watchman service, and aggressive caching. When you see 'unable to resolve module', Metro either can't find the file because the path resolution failed (e.g., missing index file, wrong extension priority) or the module is excluded via the blacklist. Common causes: stale cache, an index.js/index.ts mismatch, a symlink to a parent directory, or a module named the same as a core Node module. The error rarely means the module isn't installed—it means Metro's resolver gave up.

( 01 )Fast diagnosis

The first ten minutes — establish facts before touching code.

  • 1Run `npx react-native start --reset-cache` to clear Metro's cache and try again
  • 2Check if the module exists at the resolved path: `ls -la node_modules/[module-name]/`
  • 3Verify the package.json of the module has a valid `main` field or that an index.js/index.ts exists
  • 4Add `console.log(require.resolve('module-name'))` in your code to see where Node resolves it
  • 5Check if the module is in Metro's blacklist: look for `blacklistRE` in metro.config.js
  • 6Run `watchman watch-del-all && watchman shutdown-server` to reset file watchers
( 02 )Where to look

The specific files, logs, configs, and dashboards that usually own this bug.

  • searchmetro.config.js — watchFolders, blacklistRE, resolver.sourceExts
  • searchnode_modules/<module>/package.json — main, exports, react-native field
  • searchpackage.json — module resolution overrides in 'resolutions' or 'overrides'
  • searchbabel.config.js — module resolver plugin like 'babel-plugin-module-resolver'
  • searchWatchman logs: `watchman log-level 3` then reproduce the build
  • searchMetro's cache directory: `<project>/node_modules/.cache/metro/`
  • search.gitignore — sometimes node_modules is accidentally excluded causing symlinks to break
( 03 )Common root causes

Practical causes, not theory. These are the things you will actually find.

  • warningStale Metro cache after adding/removing a module
  • warningModule's package.json missing 'main' or 'react-native' field, or pointing to a file that doesn't exist
  • warningSymlinked local package (yarn link or npm link) that Metro can't follow
  • warningConflicting file extensions: Metro expects .js/.ts/.tsx but module only has .jsx
  • warningModule blacklisted by blacklistRE in metro.config.js (often a React Native upgrade leftover)
  • warningDuplicate module in different node_modules (e.g., hoisted vs nested) causing resolution ambiguity
( 04 )Fix patterns

Concrete fix directions. Pick the one that matches your root cause.

  • buildClear Metro cache: `npx react-native start --reset-cache` or delete node_modules/.cache/metro
  • buildFor symlinked packages, add the symlink target to `watchFolders` in metro.config.js
  • buildUpdate the module's package.json to include a 'react-native' entry that points to the correct file
  • buildEnsure file exists: add missing index file or adjust extension priority in resolver.sourceExts
  • buildTemporarily remove blacklistRE from metro.config.js to confirm it's the cause, then fix the regex
  • buildUse `npx react-native-community/cli clean` to reset all caches
( 05 )How to verify

A fix you cannot prove is a guess. Close the loop.

  • verifiedRun `npx react-native start --reset-cache` and confirm the build succeeds
  • verifiedDelete the module and reinstall: `rm -rf node_modules/<module> && npm install`
  • verifiedAdd a console.log in the module's index file to ensure it's being loaded
  • verifiedRun Metro with `--verbose` and grep for the module name to see resolution attempts
  • verifiedCheck if the error reappears after a clean clone of the repo (npx react-native run-android --reset-cache)
( 06 )Mistakes to avoid

Things that make this bug worse or harder to find.

  • warningRunning `npm install` instead of clearing Metro cache first—installing again rarely fixes resolution
  • warningAdding the module to Metro's blacklist out of frustration—it will only make it worse
  • warningManually editing node_modules—that change gets overwritten on next install
  • warningIgnoring duplicate module warnings from npm/yarn—they cause silent resolution failures
  • warningAssuming the module is missing when it's actually a symlink issue—check with `ls -la`
  • warningUsing `require` instead of `import` in a TypeScript project—Metro may handle them differently
( 07 )War story

Unable to resolve module 'react-native-maps' after adding a local library

Senior Frontend EngineerReact Native 0.70, Metro 0.76, macOS Monterey, Node 18

Timeline

  1. 10:15Developer runs `npx react-native run-ios` after merging feature branch
  2. 10:17Build fails with 'Unable to resolve module `react-native-maps` from `App.js`'
  3. 10:20Checks node_modules/react-native-maps — folder exists with all files
  4. 10:25Runs `npx react-native start --reset-cache` — no change
  5. 10:30Developer opens metro.config.js, notices a watchFolders entry pointing to a local library
  6. 10:35Commenting out watchFolders fixes the issue — local library's package.json had a 'react-native' field pointing to a non-existent file
  7. 10:40Fixes the local library's package.json by adding the correct file, reinstates watchFolders
  8. 10:45Build succeeds — root cause was a malformed package.json in a symlinked local package that confused Metro's resolver

I was integrating a shared UI library from a sibling repository using yalc (a symlink tool). After pulling latest changes, the build broke with 'Unable to resolve module react-native-maps'. The module was installed and present in node_modules, so I immediately suspected a cache issue. I cleared Metro's cache, deleted node_modules, reinstalled—nothing worked.

I opened metro.config.js and saw a watchFolders entry pointing to the local library. I commented it out, and suddenly the build worked. The problem was that Metro, when given a watchFolder, prioritized resolving modules from that folder, and the local library's package.json had a 'react-native' field pointing to a file that didn't exist (it was renamed during a refactor). Metro failed to resolve the fallback and gave up.

The fix was updating the local library's package.json to point to the correct entry file. The lesson: watchFolders can override Metro's resolution order, and any misconfiguration there will cause cryptic resolution failures. Always check symlinked packages for correct 'main' and 'react-native' fields, and use --reset-cache after any change to watchFolders.

Root cause

Local symlinked package had an incorrect 'react-native' entry in its package.json, causing Metro to fail resolution when the package was in watchFolders.

The fix

Corrected the 'react-native' field in the local library's package.json to point to the actual entry file, and re-enabled watchFolders.

The lesson

Metro's resolution with watchFolders is fragile—always verify the target package's entry points and clear cache after changes.

( 08 )How Metro Resolves Modules (and why it's different from Node)

Metro uses a custom resolver that doesn't follow Node's algorithm exactly. It prioritizes certain extensions (.js, .jsx, .ts, .tsx) and looks for an index file in directories. It also has a blacklist to exclude files like __tests__ and .graphql.

Understanding the order: 1) Check if module is a package and look for package.json->'react-native' field, then 'main'. 2) Resolve relative paths with extension priority (default .js > .jsx > .json > .ts > .tsx). 3) Use watchFolders and node_modules paths. This order is why a missing 'react-native' field can cause fallback to 'main' that may point to a dist file, but if that dist file has unsupported syntax, Metro errors.

( 09 )Cache Busting: What Actually Works

The most reliable way to clear Metro's cache is `npx react-native start --reset-cache`. This removes the cache directory at node_modules/.cache/metro. But sometimes the Watchman file watcher holds stale metadata. Run `watchman watch-del-all && watchman shutdown-server` to reset Watchman.

If using Expo, `expo start -c` resets both Metro and Watchman. For CI, always run with `--reset-cache` to avoid cache pollution between builds. I've seen cases where deleting the entire node_modules and reinstalling doesn't help—you must explicitly reset the Metro cache.

( 11 )BlacklistRE: When Metro Ignores Files You Need

The blacklistRE in metro.config.js is a regex that filters out files from the resolution graph. It's often used to exclude test files or duplicate copies of a package. However, an overly aggressive blacklist can exclude the module you need.

Common example: a project that upgraded from a React Native version that used to include a package may have a blacklist for that package's old path. Check your blacklistRE—temporarily set it to empty to see if that fixes the issue, then narrow it down.

( 12 )Extension Priority Conflicts

Metro's default extension list is ['.js', '.jsx', '.json', '.ts', '.tsx']. If a module only provides a .mjs file, Metro won't find it. You can add extensions via `resolver.sourceExts` in metro.config.js.

I've seen cases where a package ships both .js and .ts files, and the .ts file has a syntax error. Metro tries the .ts first (if .ts is in sourceExts), causing an error. The fix is to reorder extensions so .js is first, or remove .ts if not needed.

Frequently asked questions

Why does 'Unable to resolve module' happen even though the module is installed?

Most often due to Metro's cache. Run `npx react-native start --reset-cache`. If that fails, check the module's package.json for missing 'main' or 'react-native' field, or verify the file actually exists on disk.

How do I fix a symlinked local package that Metro can't resolve?

Add the symlink target's directory to `watchFolders` in metro.config.js. Also ensure the local package's package.json has correct entry fields ('main', 'react-native'). Clear Metro cache after changes.

Can a blacklistRE in metro.config.js cause this error?

Yes. If the blacklist regex matches the module path, Metro excludes it from resolution. Temporarily remove blacklistRE to test, then adjust the regex to exclude only what's necessary.

Does Metro support TypeScript's path aliases?

Not natively. You need a plugin like 'babel-plugin-module-resolver' and configure it in babel.config.js. Also add the alias paths to Metro's resolver.extraNodeModules to avoid resolution issues.

Why does the error only happen in production build but not in development?

In production, Metro bundles with different settings (e.g., minification, scope hoisting). Check if the module is being excluded by a production-only blacklist or if a conditional require fails. Also, ensure the module is installed in 'dependencies' not 'devDependencies'.