Fixing the Module Not Found Error: A Complete Guide
Tired of the 'module not found error'? Our step-by-step guide helps you diagnose and fix it in Node, React Native, webpack, and monorepos. Get unblocked now.
By Rishav
23rd Jun 2026
Last updated: 23rd Jun 2026

You hit Run, Metro starts churning, and then everything stops on a red screen that says some version of module not found. The app that worked yesterday won't open. The designer can't review the new onboarding flow. The PM is waiting on a TestFlight build. Someone says, “Did you try reinstalling node_modules?” and now the team is guessing.
That error feels simple, but in modern mobile teams it rarely is. In a React Native app, especially one living inside a monorepo, “module not found” can mean a missing package, a broken path alias, a stale Metro cache, a workspace hoisting issue, or a file that only fails once Linux sees its real casing. The frustrating part is that generic advice often points you in the wrong direction.
This guide is for product teams building mobile apps with React Native, Expo, workspaces, and shared packages. If you're a founder or PM, you'll understand what the error means and how to collect the right clues. If you're a developer, you'll have a cleaner way to isolate the cause without thrashing your config for two hours.
Why Your App Says Module Not Found
The message sounds dramatic, but it usually means something very literal. Your app asked the bundler for a file or package, and the bundler couldn't resolve where that code lives.
In a React Native workflow, that request often passes through Metro. In web builds it may go through Webpack or Vite. Your source code says import Button from '@ui/Button' or import { theme } from '../../theme', and the bundler tries to map that text to a real file on disk. If it can't, the build stops.

This is one reason the problem shows up so often. Missing dependencies and unresolved import paths account for roughly 28% of all errors flagged in public JavaScript repositories, and the npm registry grew to over 3.4 million packages by 2023, which increases the chance of misconfigured imports and version mismatches.
What the error means for the whole team
If you're not writing code every day, think of a module as a building block your app depends on. It might be a package from npm, a shared design system component, an image utility, or a local file in your project.
When the app can't find one of those blocks:
- Developers can't bundle the app.
- Designers can't review the latest UI in a simulator.
- PMs and founders can't validate a feature on a device.
- QA can't tell whether the bug is in the feature or in the build setup.
A single missing import can block all of that.
Why React Native teams get hit harder
React Native adds another layer of complexity because mobile teams often mix several resolution systems at once:
| Part of the stack | What it resolves | Common failure |
|---|---|---|
| Metro | App source files and packages | Alias or cache mismatch |
| Workspaces | Shared packages in a monorepo | Hoisted dependency not visible where expected |
| Babel config | Path aliases and transforms | Alias works in editor but not in bundler |
| Native build tooling | Platform-specific package integration | Package installed but not linked or discovered correctly |
A module not found error is rarely “just install the package” in a monorepo mobile app. The import has to make sense to every tool in the chain.
That's why two teammates can look at the same import and get different outcomes. One editor auto-completes it. Another machine builds it. CI fails. The error is about code location, but the root cause is often tool disagreement.
First Steps to Diagnosing the Problem
The worst response to a module not found error is random editing. Don't start changing aliases, moving files, and deleting lockfiles all at once. Read the error like a log, not like an accusation.
Start with the two clues that matter
Every module resolution error gives you two critical facts:
- What module is missing
- Where the bundler tried to resolve it from
If Metro says it can't find @shared/ui/Button from apps/mobile/src/screens/Home.tsx, you already know more than you think. The problem is not “the app is broken.” The problem is “this import path doesn't resolve from this file under this toolchain.”
Look for these patterns in the output:
- Package name errors like
Unable to resolve module react-native-svg - Local path errors like
Unable to resolve ../components/Header - Alias errors like
Cannot find module @shared/config - Asset errors involving images, fonts, or JSON files
Verify installation before touching config
A foundational check across environments is confirming that the package exists in the active environment. Running commands such as pip list in Python or npm ls in Node.js helps separate a missing installation from a pathing problem, as explained in this practical environment verification guide.
For React Native and monorepo projects, I'd usually run:
npm ls <package-name>
or
yarn why <package-name>
Then inspect whether the package is present where your workspace expects it.
Use this short checklist before doing anything destructive:
- Read the exact import string. Copy it from the error, not from memory.
- Check whether it's a package or local file. Those fail for different reasons.
- Run
npm ls <package-name>if it looks like a package import. - Open the filesystem and verify the target file really exists if it's a local path.
- Check recent changes in version control. A renamed file or moved package is often the actual trigger. Good Git hygiene makes this much easier to spot, and this Git workflow primer for app teams is a solid refresher if your repo history is messy.
Ask one narrowing question
Before diving deeper, ask this:
Did the import ever work on this branch, on this machine, in this workspace?
That single question splits the problem into cleaner buckets:
| Situation | Likely direction |
|---|---|
| Never worked anywhere | Bad import, missing dependency, or wrong config |
| Worked on one machine only | Local cache, local environment, or case-sensitivity issue |
| Worked before recent refactor | File move, alias drift, or broken exports |
| Fails only in CI or device build | Workspace visibility, case mismatch, or platform-specific resolution |
If you're a PM or designer helping triage, you can contribute effectively. Share the exact screen, branch, commit, and whether the failure happens on simulator, local build, or CI. That context often saves more time than another reinstall.
The Usual Suspects and Their Quick Fixes
Most module not found errors still come from a small set of repeat offenders. Before you tear apart Metro config, rule these out fast.

Compare the common failures
| Suspect | What it looks like | Fastest check | Quick fix |
|---|---|---|---|
| Typo | @componets/Button instead of @components/Button | Copy import into search | Correct the spelling |
| Missing dependency | Package import fails everywhere | npm ls <package> | Install it in the right workspace |
| Wrong relative path | ../ chain points to nowhere | Open file tree from importing file | Recalculate path or use a valid alias |
| Case mismatch | Works locally, fails in CI or cloud build | Compare import text to filename exactly | Rename file or import so casing matches |
| Wrong package boundary | Shared package imports hidden internals | Check package exports and entry points | Import from the public entry the package exposes |
One of the most common examples is embarrassingly small: ../../component/Button when the folder is components.
Typos beat sophistication more often than people admit
You can lose an hour assuming the issue is Metro when the actual problem is one missing letter. This is especially common in shared component libraries where similar names pile up fast: Card, Cards, CardList, BaseCard, UICard.
A simple comparison helps:
- Node package typo:
react-nativ-svg - React Native local typo:
../compnents/Header - Workspace typo:
@shared/desgn-tokens
If the package or path looks close to correct, don't trust your eyes. Compare it character by character.
Path style matters more in mobile projects
Relative imports are explicit, but they get brittle once folders start moving. Aliases are cleaner, but only if every tool agrees on them.
A practical rule of thumb:
- Use relative imports for nearby files within the same feature.
- Use aliases for shared packages and cross-feature imports.
- Don't mix three different alias styles in one app.
Practical rule: If TypeScript understands an alias but Metro doesn't, the alias is not actually configured. Editor success is not build success.
To make this more concrete, compare how the same issue shows up:
| Check | Plain Node app | React Native app |
|---|---|---|
| Installed package | npm ls package-name | Same |
| Local file exists | Check path in filesystem | Same |
| Alias support | tsconfig and bundler config | tsconfig, Babel, and Metro may all matter |
| Asset resolution | Webpack/Vite loader rules | Metro asset handling |
A quick visual summary helps when the team is moving fast.
Case sensitivity breaks “works on my machine”
This one catches teams moving from macOS to Linux-based CI. File naming and import path issues are a major cause of module not found errors, and letter casing must exactly match the import path because build environments can be case-sensitive. It's also a best practice to enforce case-sensitive paths in Git, as noted in Vercel's deployment troubleshooting guidance.
Example:
import Logo from './logo.png'
But the actual file is Logo.png.
macOS may let that slide in ways your deployment environment won't. The long-term fix is not “hope CI catches it.” Rename the file and import so they match exactly, then commit the change cleanly.
The quick triage order that usually works
- First, inspect the exact import text against the actual filename.
- Next, verify the package exists in the right workspace.
- Then, confirm the path type. Relative, alias, package import, or asset.
- Finally, test casing as if you were deploying to a case-sensitive environment.
If you do these four checks in order, you'll eliminate a surprising amount of noise before the main bundler work starts.
Unlocking the Bundler Black Box
When the obvious fixes don't work, the problem usually lives in the resolver. That's the part of Metro or Webpack that decides how an import string turns into a real file path.
In React Native teams, generic troubleshooting advice often falls apart. There's a real knowledge gap in modern JavaScript and React Native workflows, where module not found errors often come from bundler configuration or symlink resolution in monorepos, and many tutorials still push Python-style fixes that don't address the JavaScript toolchain. The Airbrake discussion of JavaScript module path issues is useful because it keeps the focus on resolution logic rather than generic reinstall advice.

What Metro is actually doing
Metro doesn't just look in one folder and give up. It applies rules. It reads your app entry, follows imports, resolves package boundaries, and checks configured source extensions and watch folders.
That means an import can fail even when the file exists.
Common examples:
- The alias exists in
tsconfig.jsonbut not in Babel or Metro. - The package lives in a sibling workspace, but Metro isn't watching that folder.
- A symlinked package resolves differently than a hoisted dependency.
- Cache still points at an old file tree after a rename.
The cache reset that often saves the day
If the import path is now correct and the package is present, reset the bundler before changing deeper config. Stale caches can keep old assumptions alive.
Typical commands include:
npx react-native start --reset-cache
or for Expo:
npx expo start -c
That doesn't magically fix a bad setup, but it clears false trails. I've seen teams spend ages tweaking aliases when Metro was holding onto an outdated module graph.
Reset the cache after renaming files, changing aliases, moving packages, or switching branches. Otherwise you may debug yesterday's state instead of today's code.
Where alias setups usually drift
A mobile app can have alias definitions in multiple places:
| Tool | What you may have configured |
|---|---|
| TypeScript | paths in tsconfig.json |
| Babel | module-resolver plugin aliases |
| Metro | resolver, watchFolders, custom config |
| Webpack | resolve.alias |
The trap is obvious once you've seen it. The editor resolves @shared/ui, TypeScript is happy, and imports still fail on device because Metro never got the memo.
If you're bootstrapping a project or comparing setups, a concrete baseline helps more than vague recommendations. This Expo app setup walkthrough is useful as a reference point because it shows a cleaner starting structure than many half-migrated repos.
Think beyond local development
This isn't just a mobile bundler problem. Teams run into the same pattern in serverless and packaged environments where code must be discoverable at runtime, not just present in a repo. If you've worked with deployment packaging, YayRemote's guide to Lambda layers is a good parallel example of how dependency layout affects whether code can be resolved in execution environments.
That same mindset helps in React Native. Don't ask only, “Is the package installed?” Ask, “Can this specific runtime resolve it using its own rules?”
A better debugging sequence for stubborn bundler failures
- Check the import class. Package, local path, alias, or asset.
- Reset Metro or Expo cache.
- Compare alias config across tools.
- Inspect watch folders and workspace visibility.
- Test with a direct relative import as a temporary experiment.
- Rebuild after each single change, not after five changes together.
That last part matters. Bundler debugging becomes chaotic when every experiment changes three variables at once.
Solving Monorepo and Workspace Mysteries
Monorepos make React Native teams faster when they work well. Shared UI kits, API clients, design tokens, and utility packages all stay in one repo. But they also create a version of the module not found error that looks irrational until you think in terms of package boundaries.
A common setup looks like this:
apps/mobileapps/webpackages/uipackages/configpackages/eslint-config
The mobile app imports @acme/ui/Button. The package exists. The file exists. TypeScript can jump to definition. Metro still says it can't resolve the module.
Why monorepos fail differently
In distributed systems, module not found issues often happen when a worker runs in a different environment or directory than the driver script. The Ray community discussion of worker environment mismatches is a useful analogy because monorepos have a similar visibility problem. A workspace doesn't always see dependencies the same way the project root does.
That shows up in a few ways:
- The root has a dependency, but the workspace package didn't declare it.
- Hoisting moved a package to the top-level
node_modules, and a local assumption broke. - A symlinked package exists, but Metro doesn't follow it by default in your current config.
- The shared package exports compiled files, but your app imports raw source paths.
A concrete mobile example
Say packages/ui contains a Button.tsx component that imports react-native-svg. The root repo has react-native-svg installed, so local editor tooling looks fine.
Then apps/mobile imports @acme/ui/Button, and Metro fails.
Why? Often because the UI package itself never declared react-native-svg properly, or because your bundler isn't set up to resolve the shared workspace the way TypeScript does. The dependency is “somewhere in the repo,” but not visible from the perspective that matters.
In monorepos, “installed” and “resolvable” are not the same thing.
What to inspect in a shared package
Use this checklist on the package that's being imported, not just on the app:
- Its
package.json. Does it declare the dependency it uses? - Its entry points. Are
main,module, orexportspointing to real build output? - Its file format. Is the app consuming TypeScript source, compiled JavaScript, or both?
- Its import style. Does it rely on aliases the consuming app doesn't know about?
- Its local assumptions. Does it import sibling package internals instead of public exports?
A useful sanity test is to import the package from the simplest supported public entry. If @acme/ui works but @acme/ui/src/Button fails, that's not random. Your package boundary is telling you where it expects consumers to enter.
The trade-off teams need to accept
Monorepos reduce duplication, but they demand stricter packaging discipline. You can't rely on repo-wide convenience and still expect every workspace to behave like an independent package consumer.
That means:
| Convenience habit | Safer monorepo habit |
|---|---|
| Importing from package internals | Import from documented public entry points |
| Assuming root dependencies are enough | Declare dependencies where they're used |
| Trusting editor auto-imports | Validate imports with the app bundler |
| Mixing source and build outputs casually | Standardize package build and consumption rules |
If your mobile app depends on shared workspaces, treat each package like a published library even when it never leaves your repo.
Building Resilience to Prevent Future Errors
Teams usually treat module not found errors as isolated bugs. That's too narrow. They're often signs that dependency discipline, package boundaries, and build consistency need tightening.
The most effective prevention is boring, which is why it works.
Standardize the dependency tree
Use a lockfile and protect it. Whether your team uses package-lock.json, yarn.lock, or pnpm-lock.yaml, the point is the same: everyone should resolve the same dependency graph.
In CI, prefer clean installs over best-effort installs. npm ci is stricter than npm install, and that strictness catches drift earlier.
A resilient baseline usually includes:
- Committed lockfiles so every machine resolves the same versions
- Clean CI installs instead of reusing accidental local state
- Workspace dependency declarations in the package that needs them
- One source of truth for aliases with matching bundler support
- Case-sensitive file hygiene before deployment catches it for you
Turn recurring pain into team rules
If your team has hit the same issue three times, write a short rule for it. Not a giant wiki page. A practical rule.
Examples:
- New shared packages must expose a clear public entry.
- File renames must be reviewed for casing.
- Alias changes require validation in Metro, not just TypeScript.
- Mobile builds must run at least once before merging workspace refactors.
That kind of process feels slower only until the next broken release candidate.

Reduce setup drift across the team
A lot of these errors start because one machine has a hidden advantage. An older cache. A global package. A local patch. A manually fixed config that never made it into the repo.
That's why dependency workflows matter so much. This dependency management guide for app teams is worth reviewing if your current setup relies too much on tribal knowledge and too little on repeatable project rules.
The best fix for a module not found error is often not technical. It's making the project easier to reproduce for the next person.
The goal isn't to eliminate every resolution bug forever. It's to build a mobile workflow where these issues surface quickly, fail clearly, and don't block the whole team.
If your team wants fewer build-tool detours and a faster path from idea to working mobile prototype, RapidNative is worth a look. It helps product teams generate real React Native apps with a modern stack, collaborate on working prototypes, and export clean code without getting buried in early setup friction.
Ready to build your app?
Turn your idea into a production-ready React Native app in minutes.
Free tools to get you started
Free AI PRD Generator
Generate a professional product requirements document in seconds. Describe your product idea and get a complete, structured PRD instantly.
Try it freeFree AI App Name Generator
Generate unique, brandable app name ideas with AI. Get creative name suggestions with taglines, brand colors, and monogram previews.
Try it freeFree AI App Icon Generator
Generate beautiful, professional app icons with AI. Describe your app and get multiple icon variations in different styles, ready for App Store and Google Play.
Try it freeFrequently asked questions
What is RapidNative?
RapidNative is an AI-powered mobile app builder. Describe the app you want in plain English and RapidNative generates real, production-ready React Native screens you can preview, edit, and publish to the App Store or Google Play.
Can I export the code?
Yes. RapidNative generates clean React Native and Expo code that you can export at any time. No lock-in, no proprietary format. Hand it to your developers or keep building inside RapidNative.
Is RapidNative free to use?
Yes. You can build apps on the free plan with no credit card required. Paid plans unlock unlimited AI generations, code export, and direct publishing to the App Store and Google Play.
Do I need to know how to code?
No. Most users build apps by describing what they want in plain English. Developers can drop into the code whenever they want more control, but coding is optional.
How long does it take to build an app?
Most users have a working first screen in under a minute. A full MVP usually takes a few hours instead of the weeks or months traditional development requires.