Jest 'Cannot find module': Diagnosing moduleNameMapper and Resolution Failures
Jest says 'Cannot find module' but the import works in your app build. A diagnostic walkthrough of moduleNameMapper regexes, tsconfig path drift, stale caches, and asset stubs — with ordered checks and fixes for each finding.
26 Aug 2025, 08:57 UTC

Recognizing the failure
Your tests crash before a single assertion runs. Jest prints Cannot find module '@/utils/format' from 'src/components/Table.test.js' (or a similar alias, CSS file, or package name), yet the import works fine in your app build. That mismatch — bundler resolves it, Jest does not — almost always means Jest's own module resolution is misconfigured, not that the file is missing.
Jest does not read your webpack or Vite aliases. It resolves imports using its own rules: roots, moduleDirectories, moduleNameMapper, and (for TypeScript path aliases) whatever mapping you manually keep in sync with tsconfig.json. This guide walks through the common causes, ordered checks, and the fix for each finding. Examples assume Jest 29 with a typical jsdom setup; the resolution behavior described is stable across recent major versions.
Cause and diagnostic table
| Symptom detail | Likely cause | First check |
|---|---|---|
Alias imports like @/... fail, relative imports pass | Missing or wrong moduleNameMapper entry | Compare mapper regex against the failing path |
Error mentions a .css, .svg, or image file | No asset/style mapper configured | Look for a fileMock or identity-obj-proxy entry |
| Error persists after you fixed the config | Stale Jest cache (haste map) | Run jest --clearCache and retry |
| Only fails in CI or under parallel workers | Case-sensitivity or worker-specific resolution | Reproduce locally with --runInBand |
| TypeScript path aliases fail, JS imports pass | tsconfig.json paths/baseUrl out of sync with Jest config | Diff the two configs side by side |
| Failure mentions browser globals alongside module errors | testEnvironment: 'node' where jsdom is expected | Check the environment setting, not the mapper |
Ordered checks
1. Clear the cache first
Jest caches its module map between runs. A correct config change can appear to do nothing because the stale map is still in use. Run this from your project root (no special permissions needed):
npx jest --clearCache
npx jest path/to/failing.test.jsIf the error disappears, you are done — the config was already right. Do not disable the cache permanently; it exists to keep large suites fast.
2. Get the exact path Jest tried
Run with verbose output to see which file Jest attempted to load and from which importer:
npx jest --verbose path/to/failing.test.jsThe "from '...'" portion of the error tells you the importing file; the quoted module specifier is what your mapper must match. Write both down before touching the config.
3. Test your mapper regex in isolation
moduleNameMapper keys are regular expressions matched against the raw import specifier, and values use $1-style capture substitution. A frequent bug: forgetting to anchor the pattern, so ^@/(.*)$ is written as @/(.*) and partially matches, or the substitution points at the wrong directory. Verify the mapping by evaluating it mentally against the specifier from step 2 — or paste both into a regex tester. Then check that the substituted path, relative to rootDir, actually exists on disk.
4. Confirm resolution outside Jest
For TypeScript aliases, confirm the compiler itself resolves the import, separating a tsconfig problem from a Jest problem:
npx tsc --noEmitIf tsc passes but Jest fails, the configs are out of sync — Jest never reads paths automatically unless you use a helper such as ts-jest's pathsToModuleNameMapper.
5. Reproduce serially
Some resolution failures only surface under parallel workers. Run npx jest --runInBand to execute tests in a single process. If the error vanishes, suspect case-sensitivity differences between your local filesystem and CI, or a worker-scoped config override.
Fixes tied to findings
Alias imports fail: add or correct the mapper
In jest.config.js:
module.exports = {
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
},
};<rootDir> is the directory Jest treats as the project root (usually where the config lives). If your tsconfig.json sets "baseUrl": "." with "paths": { "@/*": ["src/*"] }, the mapper above mirrors it. Keep the two in sync deliberately — this is the most common drift point.
CSS or asset imports fail: stub them
Jest executes imported files as JavaScript, so a stylesheet import throws a resolution or syntax error. Map styles to a proxy and binary assets to a stub:
moduleNameMapper: {
'\\.(css|less|scss)$': 'identity-obj-proxy',
'\\.(svg|png|jpg)$': '<rootDir>/test/fileMock.js',
},identity-obj-proxy (an npm package you must install) returns the class name as its own value, which keeps class-name assertions working. fileMock.js is a one-line file you create: module.exports = 'test-file-stub';. Note the escaped dots in the regexes — an unescaped . matches any character and can swallow unintended imports.
tsconfig and Jest disagree: derive the mapper
With ts-jest, generate the mapper from paths instead of maintaining two copies:
const { pathsToModuleNameMapper } = require('ts-jest');
const { compilerOptions } = require('./tsconfig');
module.exports = {
preset: 'ts-jest',
moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, {
prefix: '<rootDir>/',
}),
};The prefix matters when baseUrl is not the project root; omitting it is a classic source of paths that resolve one directory off.
Browser-global failures masquerading as module errors
If the suite also complains about window or document, set testEnvironment: 'jsdom' (requires the jest-environment-jsdom package in Jest 28+). Fixing the environment often makes the apparent module error disappear because the failing import was a browser-only package.
Escalation criteria
Stop tweaking the mapper and look elsewhere when: the error names a package in node_modules rather than a local path (check transformIgnorePatterns — an untranspiled ESM dependency throws a similar-looking error); the failure only occurs in CI (compare Node versions and check out the repo with a case-sensitive filesystem in mind); or --clearCache, serial execution, and a verified mapper all pass locally but a teammate still fails (compare lockfiles and Jest versions).
Limitations
Regex behavior in moduleNameMapper is stable, but package-specific quirks — ESM-only dependencies, monorepo projects configs, custom resolvers — can produce identical error text with different root causes. The verification steps above (cache clear, verbose run, tsc --noEmit, --runInBand) are the reliable way to narrow it down before changing configuration.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.