New to Rust? Grab our free Rust for Beginners eBook Get it free →
Node.js Cannot Find Module Error: The Definitive Fix Guide for 2026

If you have spent any time working with Node.js, you have almost certainly hit the infamous “Cannot find module” error. It shows up as Error: Cannot find module 'some-package' or Error: Cannot find module './utils', and it has a way of making you feel like you are starting over. I have seen it tank deployment timelines, frustrate senior engineers, and send juniors into a spiral of reinstalling npm packages they did not need to reinstall in the first place. This guide is the most thorough breakdown you will find on why this error happens, where Node.js looks for modules, and exactly how to fix each variant of it.
The error is not random. Node.js has a specific module resolution algorithm, and the “Cannot find module” error fires whenever that algorithm exhausts every possible path without finding a match. That algorithm is well-documented, but the error messages do not tell you which path was checked last or why it failed. That silence is what makes debugging painful. This guide fixes that for you.
I have personally encountered every variant of this error across Linux development machines, Windows workstations, Docker containers, and CI pipelines. I will walk you through the resolution order, the case sensitivity traps, the NODE_PATH gotchas, the symlink nightmares, and everything in between. By the end, you will be able to diagnose any “Cannot find module” error in under two minutes.
For related reading, see my guide on how to install Node.js on Ubuntu, my deep dive on Node.js vs Python for backend development, and my tutorial on building a REST API with Node.js and Express.
TLDR
- The error fires when Node.js module resolution finds no match across all searched paths
- Common causes: wrong working directory, case-sensitive paths on Linux, stale symlinks, NODE_PATH misconfig, npm link misuse
- Debug with
require.resolve.paths(moduleName)to see exactly which paths Node.js checks - Global modules live in a different tree and need
NODE_PATHor proper linking - Docker builds need extra care with volume mounts and case-sensitive filesystems
- Prevention: use package.json scripts, lock versions, avoid global installs in production
- Use
npm lsandnpm ls --depth=0to trace missing dependencies
What Does “Cannot Find Module” Actually Mean
When Node.js executes a require() or import statement, it does not just look in one place. It runs through a defined search sequence. If none of the steps in that sequence find a file or package that satisfies the request, Node.js throws a MODULE_NOT_FOUND error. The message looks simple, but the underlying process involves multiple filesystem lookups, directory climbs, and environment variable checks.
The error is a symptom, not a diagnosis. Seeing “Cannot find module” tells you resolution failed. It does not tell you why, or which specific path was the last one checked. That is exactly why so many engineers reinstall npm packages as a first reflex. It rarely helps because the problem is usually in the resolution path, not the package itself.
In this guide, I will give you the mental model and the tools to pinpoint the root cause every single time. No more guessing. No more rm -rf node_modules && npm install as a superstition.
How Node.js Module Resolution Works (The Algorithm)
Node.js resolves modules in a specific order. Understanding this sequence is the single most important thing you can do to eliminate these errors. The algorithm differs slightly between require() and import, but the core logic is shared.
When you call require('./utils/helpers'), Node.js starts at the file that issued the call and works through these steps in order: file-based resolution first (exact match, then with extensions like .js, .json, .node), then directory-based resolution (checking for an index.js or package.json with a main field), then climbing up the directory tree to node_modules folders at each level, and finally checking the paths in the NODE_PATH environment variable.
The algorithm continues climbing until it hits the root filesystem. If no match is found by then, you get the error. Here is the sequence in plain terms:
- Is
./utils/helpersa file? Check.js,.json,.node. - Is
./utils/helpersa directory? Look forindex.jsorpackage.jsonmain. - Check
./node_modules/utils/helpers. - Climb to
./node_modules/utils/helpers/index.js. - Climb to
../node_modules/utils/helpers. - Repeat until filesystem root.
- Check
NODE_PATHenvironment variable directories. - Check global module directories.
For a complete Node.js installation guide, see this detailed tutorial on codeforgeek.com.
One key detail: Node.js does not search every node_modules folder on the system. It follows a single chain upward from the requiring file. This means if your module is nested in a subdirectory that has its own node_modules, sibling directories are not checked. This catches a lot of people off guard.
Why Your Working Directory Is the First Place Node.js Looks
Node.js module resolution is path-based, and the starting point is always the file that contains the require() call. Not the project root. Not the directory you ran node from. The file. This distinction matters more than most engineers realize.
Consider a project with this structure:
my-project/
src/
utils/
logger.js
app.js
node_modules/
express/
If src/app.js calls require('./utils/logger'), Node.js resolves relative to src/, not my-project/. It will look for src/utils/logger.js. If you move app.js to the root and it still references './utils/logger', the resolution path changes entirely. The relative path in a require statement is always resolved from the file that contains it, not from the project root or the current terminal directory.
This trips up teams working with monorepos or nested scripts. A script in scripts/deploy.js that references '../config' behaves differently when run from the repo root versus when run from the scripts/ directory. Always use absolute module IDs (package names from npm) for shared code that lives in a central location.
Case Sensitivity: The Linux Trap That Windows Misses
This is one of the most insidious causes of “Cannot find module” errors, and it almost exclusively affects developers who switch between operating systems or deploy to Linux servers from Windows or macOS machines.
Linux filesystems are case-sensitive. myModule.js and mymodule.js are two completely different files. Windows and macOS (by default with HFS+) are case-insensitive. They treat those as the same file. This means code that works perfectly on your Windows laptop can break silently on a Linux CI runner or production server.
I once spent three hours debugging a broken build where the file was named DbConnect.js but the require statement read require('./dbconnect'). It worked on every engineer’s machine (Windows) and failed on the Linux CI container. The fix was a rename. Always match case exactly on the filesystem and in your require statements, especially for custom local modules.
Use this table to keep track of how your OS handles file casing:
- Windows (NTFS): Case-insensitive.
config.jsonandConfig.jsonare identical. - macOS (HFS+ or APFS default): Case-insensitive. Same behavior as Windows.
- Linux (ext4, XFS, Btrfs): Case-sensitive. Every character in the filename matters.
- Docker containers (most base images): Case-sensitive, inheriting the host kernel filesystem.
If you develop on macOS or Windows and deploy to Linux, use a CI pipeline that tests on a Linux container before merging. This catches case sensitivity bugs before they reach production.
Wrong node_modules Location: The Symptom and the Cure
The most common cause of “Cannot find module” errors is a missing or misplaced node_modules directory. When Node.js climbs the directory tree looking for modules, it stops when it finds a node_modules folder. If the package you need is not in that folder, the climb continues upward. When it reaches the root without finding a match, the error fires.
The problem is usually one of three things. First, npm install was never run, so the package was never downloaded. Second, npm install ran in the wrong directory, placing the package in a node_modules folder that is not in the resolution chain for the file that needs it. Third, the package exists but in a parent directory that is not in the climb path because of how your project is structured.
Check your project structure with npm ls. Run it from the directory where the requiring file lives:
npm ls lodash
If it shows UNMET DEPENDENCY or nothing at all, the package is not in the resolution tree for that location. Run npm install in the correct directory, or use a workspace configuration to ensure packages are visible across your project.
The NODE_PATH Environment Variable: When and How to Use It
Node.js consults the NODE_PATH environment variable as part of its module resolution algorithm. This variable should contain a list of directories separated by the platform path separator (: on Unix, ; on Windows). Node.js searches these directories after exhausting all the node_modules climbing steps.
One practical use case is global modules. If you install a package globally with npm install -g some-package, it goes into a system directory that is not automatically in the Node.js resolution path. You can add that directory to NODE_PATH to make global packages available to require:
export NODE_PATH=$(npm root -g) node my-script.js
On Windows, the equivalent command would be:
set NODE_PATH=%APPDATA%\npm\node_modules node my-script.js
I recommend using NODE_PATH only in development scripts or local tooling. Avoid it in production code because it creates implicit dependencies that are hard to track. The cleaner approach for production is to install packages locally in each project or use a monorepo toolchain that handles linking for you.
How npm link Breaks (and How to Fix It)
The npm link command is designed to let you develop a local package and use it across projects without publishing to npm. It creates a symlink from the global node_modules directory to your local package. It sounds perfect, but it has rough edges that cause “Cannot find module” errors in ways that are hard to debug.
The first failure mode is broken symlinks after relocating the source package. If you move the package directory after running npm link, the symlink points to a path that no longer exists. Node.js will look for the module, find the symlink, try to follow it, and fail. The error message will say “Cannot find module” even though the symlink technically exists.
The second failure mode is forgetting that npm link is per-machine. If you share your project with a teammate or move it to a CI environment, the link is not there. The package is truly missing in that environment. I have seen builds pass on a developer’s machine and fail in CI because the CI runner never had the link created.
The third failure mode is version mismatches. If the linked package uses a different version of a shared dependency than the consuming project, Node.js might resolve the dependency from the wrong node_modules tree, causing confusing errors that look like module-not-found issues but are actually version conflicts.
For managing complex Node.js projects, read my guide on structuring Node.js projects for scale.
For local development, prefer using npm workspaces or yarn workspaces. They handle linking reliably and keep everything in the project directory where CI and other developers get it automatically. Reserve npm link for quick one-off local module experiments, and verify the symlink is intact before debugging module resolution issues.
Debugging with require.resolve.paths()
Node.js exposes the module resolution paths through a built-in API. This is one of the most powerful debugging tools available and it is tragically underused. The require.resolve.paths(moduleName) function returns an array of all directories Node.js will search when trying to resolve a given module name.
Here is how you use it. If you are getting “Cannot find module ‘my-custom-lib'”, run this in a Node.js REPL or a script:
console.log(require.resolve.paths('my-custom-lib'));
The output will be an array of absolute paths. Node.js checks them in order. The last path is typically the global module directory. If your module should be in a local node_modules folder and it is not in the output, that tells you the directory structure is not what you expected.
For relative requires, you can inspect the resolution chain from a specific file:
// In a script, run:
const path = require('path');
const Module = require('module');
// From the file that is failing:
const searchPaths = Module._resolveFilename('./utils/helpers', {
id: require.main.filename,
filename: require.main.filename,
paths: Module._nodeModulePaths(path.dirname(require.main.filename))
});
console.log(searchPaths);
This outputs the exact paths Node.js will check when resolving ./utils/helpers from your main file. Use it to confirm whether a directory you think should be searched actually is. I have saved hours of debugging time with this alone.
Global Modules: Where They Live and Why They Can Be Invisible
Installing a package globally with npm install -g puts it in a system directory that is outside the default Node.js module resolution chain. By default, Node.js does not look in global directories when resolving modules. This surprises a lot of people who expect global installs to just work.
The location of global packages varies by system:
- Linux:
/usr/local/lib/node_modulesor~/.npm-global - macOS:
/usr/local/lib/node_modules - Windows:
%AppData%\npm\node_modules
When a module is installed globally and you run node script.js, Node.js will not find it unless NODE_PATH is set or the global directory is explicitly added to the module search paths. You can verify the global module path with:
npm root -g
Then add that path to your script or shell environment. The cleanest way to handle global modules in a project context is to avoid them entirely for project code. Use global installs only for CLI tools (like eslint, prettier, or typescript) and install all runtime dependencies locally.
Symlinks: When Node.js Follows a Broken Path
Symlinks are files that point to other files or directories. Node.js will follow symlinks during module resolution, which is normally what you want. But if the target of the symlink is moved, deleted, or points to an incorrect path, Node.js will try to resolve the module and fail with a “Cannot find module” error that can be misleading.
Pn workspaces use symlinks to connect local packages to the root node_modules directory. This is efficient and usually works seamlessly. But if a workspace package is removed from the workspace configuration without being uninstalled, the symlink in node_modules becomes a dangling pointer. The module appears to be there (it shows up in ls node_modules) but following it leads nowhere.
You can detect dangling symlinks with:
find node_modules -type l -exec sh -c 'test -e "$1" || echo "BROKEN: $1"' _ {} \;
Or on macOS:
find node_modules -type l -printf '%p -> %l\n' | grep " -> $"
If you find broken symlinks, remove them with rm and reinstall the package in that location. In a workspaces setup, run the workspace install command again to rebuild the links properly.
Package.json main Field: When It Points Nowhere
Every npm package has a package.json file. The main field in that file tells Node.js which file to load when the package is required. If that field points to a file that does not exist, you get “Cannot find module” even though the package directory exists in node_modules.
This happens more often than you would think, especially with older packages that have not been updated for Node.js module standards, or when a build step that generates the main entry point fails silently. Some packages use a type field pointing to an ES module exports field instead, and if the main field is still present but incorrect, Node.js tries main before exports in some configurations.
Check the main field of the problematic package:
cat node_modules/some-package/package.json | grep -E '"main"|"exports"'
Verify the file it points to actually exists in the package directory. If it does not, the package installation is corrupted. Delete the package directory and reinstall it:
rm -rf node_modules/some-package npm install some-package
If the problem recurs, the package itself may have a publishing issue. Check the npm registry page for the package or look at the GitHub repository to see if there is a known issue.
npm ls: The Dependency Detective
When you are not sure whether a package is installed, or which version is installed, or why npm is not finding it, npm ls is the command to reach for. It lists the entire dependency tree for your project and tells you exactly what is present and what is missing.
For a specific package:
npm ls express
If express is not found in the tree, you will see it marked as UNMET DEPENDENCY. If it is found but in a nested location (not at the root), npm ls shows the full nested path so you can understand why a file in one subdirectory might not see a package installed in another.
For a broad overview of what is missing:
npm ls --depth=0
To learn more about npm dependency management, see my Node.js setup guide.
This shows only the top-level dependencies and highlights any that are unmet or broken. Run this whenever you pull a fresh copy of a project and before you start debugging any module resolution issue. A clean npm ls output is often all you need to confirm the problem is elsewhere.
Windows vs Linux: Side-by-Side Comparison
If you work across operating systems or deploy to Linux servers, understanding the differences in how Node.js behaves on each is critical for avoiding “Cannot find module” errors. Here is a direct comparison table covering the most relevant differences.
| Behavior | Linux | Windows |
|---|---|---|
| Filename case sensitivity | Case-sensitive. Config.js and config.js are different files. |
Case-insensitive by default. Both refer to the same file. |
| Path separator in NODE_PATH | Colon (:). Example: /usr/local/lib:/home/user/lib |
Semicolon (;). Example: C:\Users\...\node_modules;C:\Program Files\node_modules |
| Default global module path | /usr/local/lib/node_modules |
%AppData%\npm\node_modules |
| Symlink behavior | Native symlinks supported. Can also use symbolic links. | Symlinks require elevation or developer mode. Junction points are often used instead. |
| Module resolution climbing | Follows Unix directory semantics. Root is /. |
Follows drive letters. Can climb across drives differently. |
| npm install behavior | Preserves exact casing of package names in filesystem. | May normalize casing in some operations. |
| Script file extensions | Requires explicit .js extension in require statements for local files. |
Can sometimes resolve .js without explicit extension in rare edge cases. |
Docker and Container Considerations
If you are containerizing a Node.js application, follow my Docker and Node.js application tutorial.
Running Node.js in Docker adds a layer of complexity to module resolution. Most base images are Linux-based and inherit case-sensitive filesystem behavior, even if your development machine is Windows or macOS. This means bugs that do not show up on your local machine will surface in your containerized build.
The most common Docker-specific cause of “Cannot find module” errors is volume mounts. If you mount your local node_modules directory into a container, the host filesystem case sensitivity applies, not the container filesystem. If your package names or paths have case mismatches relative to what the container expects, things break. Always run npm install inside the container, using a package.json and package-lock.json, rather than mounting a host machine’s node_modules.
Another common issue is running npm install as root inside a container. By default, npm creates a node_modules directory owned by root. If your application runs as a non-root user (which is best practice), it may not be able to read the installed packages, producing errors that look like “Cannot find module” but are actually permission errors.
Use a multi-stage Docker build. In the first stage, install all dependencies with full permissions. In the second stage, copy only the production files and set the correct runtime user:
FROM node:20-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm ci FROM node:20-alpine WORKDIR /app COPY --from=builder /app/node_modules ./node_modules COPY . . USER node CMD ["node", "src/index.js"]
This approach keeps your node_modules clean, consistent, and inside the container where case sensitivity and permissions are fully controlled.
The exports Field in package.json: A Modern Trap
Modern packages use the exports field in package.json to define entry points for different contexts (Node.js vs browser, ESM vs CommonJS). If a package has an exports field and you are trying to require it in a way that the exports field does not allow, Node.js will throw a “Cannot find module” error even if the file physically exists in the package directory.
The exports field acts as a gatekeeper. If it is present, Node.js will only use entry points listed in it. The old main field is ignored for that package when exports is defined.
This matters when you are trying to require a subpath within a package or use a file that is not exported. Some packages export only the main entry point and do not expose internal files. If you try to import from an internal file that is not in exports, you get the error:
Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: Package subpath './internal/utils' is not defined by exports in 'some-package'
This is a cousin of the “Cannot find module” error with slightly different wording. The fix is the same: do not reach into packages for internal files. If you need something from a package, import it through the official exports. If the export you need is missing, open an issue on the package repository.
ESM and import Statements: Common Gotchas
For migrating from CommonJS to ESM, see my Node.js tutorial series.
If you are using ES modules (import instead of require) with Node.js, there are additional resolution rules that differ from CommonJS. The most important one is that import requires full file extensions in most configurations. With CommonJS, you can write require('./utils/logger') and Node.js will find logger.js automatically. With ES modules, you typically need import ... from './utils/logger.js'.
This catches people migrating from Babel or TypeScript setups where the build step handled extension normalization. When the same code runs directly in Node.js without transpilation, the missing .js extension causes a “Cannot find module” error. Node.js ESM has been gradually relaxing this requirement, but support varies by Node.js version and module configuration.
Another ESM-specific issue is the type field in package.json. If your project has "type": "module", all .js files are treated as ES modules. If a dependency or local module does not have proper ESM support or the correct export configuration, Node.js will fail to resolve it with an error that can look like a missing module but is actually an incompatibility.
peer Dependencies and Missing Transitive Dependencies
npm has three categories of dependencies: dependencies, devDependencies, and peerDependencies. The first two are installed automatically by npm. Peer dependencies are not. They are expected to be installed by the consumer of your package. If a peer dependency is missing, you might get a “Cannot find module” error that comes from code deep in a transitive dependency.
Use npm ls to inspect the full tree and identify peer dependency gaps:
npm ls --all | grep peer
If a peer dependency is missing, install it explicitly in your project or in the package that declares it. Most commonly, this shows up when using UI component libraries that expect a specific version of React or Vue to be installed at the application level.
Clean Install vs CI Cache Issues
Sometimes “Cannot find module” errors appear only in CI and not locally. This almost always comes down to a difference in the installed packages. Locally, you might have extra packages that were installed but are not in package.json. Or your CI image has a different npm version that resolves dependencies slightly differently.
Always use npm ci instead of npm install in CI environments. npm ci does a clean install from package-lock.json, ignoring any existing node_modules and installing exactly what is locked. This eliminates the drift between your local environment and CI environment.
Verify your lockfile is committed and up to date. If it is stale (because npm install was run without updating the lock), CI might resolve different versions than you have locally, causing the CI build to pull different transitive dependencies that are incomplete or broken.
Watch Out for Circular Dependencies
For more on Node.js debugging, see my debugging Node.js applications tutorial.
Circular dependencies can produce “Cannot find module” errors that are intermittent and depend on load order. A circular dependency happens when Module A requires Module B, and Module B requires Module A. Node.js resolves this by returning a partially-executed module object, but if your code tries to access something that has not been initialized yet, it gets undefined. If your code then tries to call a method on that undefined value, it can look like the module does not exist.
The error message in these cases is not always clear. Instead of “Cannot find module”, you might see “Cannot read property of undefined” or similar. The fix is to restructure your module relationships to avoid the cycle. Common patterns include moving shared code to a third module that neither depends on, or using dependency injection to pass the module reference at runtime rather than requiring it at the top of the file.
Prevention Checklist: How to Never See This Error Again
Building a production Node.js app? See my Node.js best practices guide.
I have put together a checklist of practices that, if followed consistently, will eliminate “Cannot find module” errors from your projects. This is the summary I wish someone had given me five years ago.
- Always use
npm cifor reproducible installs in CI. Usenpm installonly for local development when you are intentionally updating the lockfile. - Lock your dependency versions in
package-lock.json. Never leave production dependencies unlocked. - Use npm workspaces or yarn workspaces for monorepos. They handle linking automatically and reliably.
- Never require files outside your project tree without using NODE_PATH intentionally and documented.
- Use relative imports only for files within the same logical module. Use package names for cross-module references.
- Add a postinstall script to your package.json that validates the dependency tree after every install.
- Test on Linux before deploying. Use GitHub Actions, GitLab CI, or any CI that runs on Linux containers.
- Never edit
node_modulesdirectly. If something is wrong in node_modules, fix it in your source, not in the installed copy. - Use
require.resolve.paths()whenever you encounter a mysterious resolution failure. - Keep your Node.js version consistent across development machines and CI using
.nvmrcorenginesfield in package.json.
Common Error Messages and What They Mean
Different module resolution failures produce slightly different error messages. Knowing how to read them narrows down the cause immediately.
Error: Cannot find module './relative-path' means a relative require failed. Node.js looked for a file relative to the requiring file and found nothing. Check the path spelling, case, and extension.
Error: Cannot find module 'package-name' means a package name resolve failed. Node.js checked every node_modules directory in the climb chain and did not find it. Run npm ls package-name to check if it is installed anywhere in the tree.
Error [ERR_PACKAGE_PATH_NOT_EXPORTED] means a package exists but does not export the subpath you requested. Check the package exports field.
Error: Cannot find module 'module-name' with a code of MODULE_NOT_FOUND is the generic form. Use require.resolve.paths('module-name') to inspect the full search list.
These distinctions matter. The first step in debugging any module error is to identify which of these patterns you are seeing, then use the corresponding resolution technique to find the gap.
Frequently Asked Questions
Q: I ran npm install and the package is in package.json but I still get Cannot find module. What gives?
A: Run npm ls to verify the package is actually in the dependency tree. If it shows as UNMET, there might be a version conflict or a peer dependency issue preventing installation. Try npm install (without ci) to get more verbose output. Check if there is an npm error log in your system temp directory that shows what went wrong during installation.
Q: My code works on my machine but not in Docker. Why?
A: This is almost always a case sensitivity or permissions issue in Docker. Verify the image uses a Linux base, run npm install inside the container (not from a volume mount), and ensure the user running Node.js has read access to node_modules. Also check that your package.json scripts use the correct path separators for Linux (forward slashes only).
Q: What is the difference between require.resolve and require.main?
A: require.resolve('module-name') returns the resolved absolute path of the module without actually loading it. require.main is a reference to the main module of the current Node.js process. You can combine them to debug resolution from your main entry point: require.resolve.paths('module-name') called from your main file context shows the full search list.
Q: I linked a local package with npm link but it still cannot be found. What should I check?
A: First verify the symlink exists and is not broken: ls -la node_modules/package-name. Then check that NODE_PATH includes the global node_modules directory: npm root -g. If the symlink is broken, recreate it. If NODE_PATH is not set, set it or use a workspaces setup instead, which is more reliable for local linking.
Q: Can NODE_PATH cause problems if it is too broad?
A: Yes. If NODE_PATH includes many directories, Node.js will search all of them for every module resolution, which can cause performance issues and unexpected behavior if multiple directories contain packages with the same name. Use NODE_PATH narrowly and intentionally. A better long-term approach for development tooling is to add directories to the module search paths programmatically in specific scripts rather than globally via the environment variable.
That covers every major cause and fix for “Cannot find module” errors in Node.js. Bookmark this guide. You will need it.




