Skip to content

Fix: ESLint import/no-unresolved Error (Module Exists but ESLint Can't Find It)

FixDevs · (Updated: )

Part of:  JavaScript & TypeScript Errors

Quick Answer

How to fix ESLint's import/no-unresolved errors when modules actually resolve correctly — configure eslint-import-resolver-typescript, fix path alias settings, and handle node_modules that ESLint cannot find.

The Error

ESLint reports import errors even though the imports work correctly in the application:

error  Unable to resolve path to module '@/components/Button'  import/no-unresolved
error  Unable to resolve path to module '../utils/helpers'     import/no-unresolved
error  Unable to resolve path to module 'some-package'         import/no-unresolved

Or TypeScript path aliases are not recognized:

error  Unable to resolve path to module '@components/Header'   import/no-unresolved
error  Unable to resolve path to module '~/utils'              import/no-unresolved

Or after adding a new package:

error  Unable to resolve path to module 'date-fns'             import/no-unresolved

The build and runtime work fine — only ESLint reports the error.

Why This Happens

The import/no-unresolved rule comes from eslint-plugin-import. It resolves module paths using its own resolver — separate from TypeScript’s module resolution, webpack’s resolve, or Node.js’s require(). By default it uses Node.js resolution rules (require.resolve semantics) and does not understand:

  • TypeScript path aliases (@/, ~/, @components/) defined in tsconfig.json
  • webpack aliases defined in webpack.config.js
  • Vite aliases defined in vite.config.js
  • Packages with only type definitions or non-standard exports fields
  • Monorepo packages linked via workspaces
  • ESM-only packages whose package.json uses "type": "module" and conditional exports
  • TypeScript paths with extensionless imports that the bundler rewrites but the default resolver does not

The fix is to install and configure a resolver that matches how your bundler or TypeScript resolves modules. The settings['import/resolver'] block tells eslint-plugin-import which resolver to use, and ESLint chains multiple resolvers in order — useful for projects that import both TypeScript-aliased paths and Node packages.

Worth understanding the resolution order: when ESLint evaluates import foo from '@/utils', it walks the configured resolvers top-to-bottom. The TypeScript resolver tries tsconfig.paths first, then falls back to Node-style lookup. If none of them produce a file path, the rule fires. This is why a partial configuration — TypeScript resolver installed but paths not set in tsconfig.json — produces the same error as no resolver at all.

Fix 1: Install and Configure eslint-import-resolver-typescript

For TypeScript projects, this is the standard fix — it makes eslint-plugin-import use TypeScript’s module resolution:

npm install --save-dev eslint-import-resolver-typescript

ESLint flat config (eslint.config.js — ESLint 9+):

import importPlugin from 'eslint-plugin-import';
import tsParser from '@typescript-eslint/parser';

export default [
  {
    plugins: { import: importPlugin },
    languageOptions: {
      parser: tsParser,
    },
    settings: {
      'import/resolver': {
        typescript: {
          alwaysTryTypes: true, // Always try @types/* packages
          project: './tsconfig.json',
        },
      },
    },
    rules: {
      'import/no-unresolved': 'error',
    },
  },
];

Legacy .eslintrc.js format:

// .eslintrc.js
module.exports = {
  parser: '@typescript-eslint/parser',
  plugins: ['import'],
  settings: {
    'import/resolver': {
      typescript: {
        alwaysTryTypes: true,
        project: './tsconfig.json',
      },
    },
  },
  rules: {
    'import/no-unresolved': 'error',
  },
};

For monorepos with multiple tsconfig files:

settings: {
  'import/resolver': {
    typescript: {
      alwaysTryTypes: true,
      project: [
        './tsconfig.json',
        './packages/*/tsconfig.json',
        './apps/*/tsconfig.json',
      ],
    },
  },
},

Pro Tip: In a pnpm or Yarn workspaces monorepo, set alwaysTryTypes: true and list every workspace tsconfig.json explicitly. The resolver does not auto-discover workspace packages — if you skip a package’s tsconfig, all imports from that package will produce import/no-unresolved. The glob pattern (./packages/*/tsconfig.json) is evaluated relative to the ESLint config file, not the working directory.

Fix 2: Configure Path Aliases Correctly in tsconfig.json

The resolver reads path aliases from tsconfig.json. Ensure they are defined there — not just in Vite or webpack config:

// tsconfig.json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],
      "@components/*": ["src/components/*"],
      "@utils/*": ["src/utils/*"],
      "@hooks/*": ["src/hooks/*"],
      "~/*": ["src/*"]
    }
  }
}

After updating tsconfig.json, restart your ESLint server (in VS Code: Ctrl+Shift+P → “ESLint: Restart ESLint Server”).

Verify the alias resolves correctly:

# Use eslint with --debug to see resolver output
npx eslint --debug src/App.tsx 2>&1 | grep -i "resolver\|alias\|resolve"

Fix 3: Use eslint-import-resolver-alias for Non-TypeScript Projects

For JavaScript projects using webpack or Vite with path aliases:

npm install --save-dev eslint-import-resolver-alias
// .eslintrc.js
module.exports = {
  settings: {
    'import/resolver': {
      alias: {
        map: [
          ['@', './src'],
          ['@components', './src/components'],
          ['@utils', './src/utils'],
        ],
        extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'],
      },
    },
  },
};

Or use eslint-import-resolver-vite for Vite projects:

npm install --save-dev vite-plugin-eslint eslint-import-resolver-vite
// .eslintrc.js
module.exports = {
  settings: {
    'import/resolver': {
      vite: {
        configPath: './vite.config.ts',
      },
    },
  },
};

Fix 4: Fix node_modules Resolution Failures

If ESLint cannot resolve a package you have installed:

Check the package actually exists:

ls node_modules/date-fns
ls node_modules/@types/date-fns

Add node to the resolver list:

// .eslintrc.js
module.exports = {
  settings: {
    'import/resolver': {
      node: {
        extensions: ['.js', '.jsx', '.ts', '.tsx'],
        moduleDirectory: ['node_modules', 'src'],
      },
      typescript: {
        alwaysTryTypes: true,
      },
    },
  },
};

For packages with non-standard exports fields (ESM-only packages):

Some modern packages use the exports field in package.json instead of main. ESLint’s resolver may not support this:

settings: {
  'import/resolver': {
    typescript: {
      alwaysTryTypes: true,
      extensionAlias: {
        '.js': ['.ts', '.tsx', '.js', '.jsx'],
      },
    },
  },
},

Add to import/ignore for packages you cannot resolve:

rules: {
  'import/no-unresolved': ['error', {
    ignore: [
      '^virtual:', // Vite virtual modules
      '^@astro/',  // Astro built-in modules
    ],
  }],
},

Fix 5: Fix for Specific Frameworks

Next.js — aliases and built-in modules:

npm install --save-dev eslint-import-resolver-typescript
// .eslintrc.js
module.exports = {
  extends: ['next/core-web-vitals'],
  settings: {
    'import/resolver': {
      typescript: {
        alwaysTryTypes: true,
        project: './tsconfig.json',
      },
    },
  },
};

Next.js’s @/ alias is defined in tsconfig.json by default when created with create-next-app — the resolver picks it up automatically.

Vite + React — handle Vite-specific virtual modules:

// .eslintrc.js
module.exports = {
  settings: {
    'import/resolver': {
      typescript: { alwaysTryTypes: true },
    },
  },
  rules: {
    'import/no-unresolved': ['error', {
      ignore: ['^virtual:'],  // Vite virtual modules like 'virtual:pwa-register'
    }],
  },
};

React Native — resolve .ios.js and .android.js extensions:

settings: {
  'import/resolver': {
    node: {
      extensions: [
        '.ios.js', '.android.js',
        '.ios.ts', '.android.ts',
        '.js', '.jsx', '.ts', '.tsx',
      ],
    },
  },
},

Fix 6: Disable the Rule for Specific Lines or Files

If you have a module that legitimately cannot be resolved by ESLint (e.g., environment-specific virtual module):

// Disable for a single line
import styles from './app.module.css'; // eslint-disable-line import/no-unresolved

// Disable for entire file
/* eslint-disable import/no-unresolved */
import { plugin } from 'some-virtual-module';
/* eslint-enable import/no-unresolved */

Disable in .eslintrc for a path pattern:

module.exports = {
  overrides: [
    {
      files: ['*.stories.tsx', '*.test.tsx'],
      rules: {
        'import/no-unresolved': 'off', // Test files can import test utilities freely
      },
    },
  ],
};

Fix 7: Configure for pnpm and Yarn Workspaces

Workspace-linked packages live as symlinks inside node_modules. The default Node resolver follows symlinks correctly, but some resolver configurations break on them:

// eslint.config.js — for pnpm workspaces
import importPlugin from 'eslint-plugin-import';

export default [
  {
    plugins: { import: importPlugin },
    settings: {
      'import/resolver': {
        typescript: {
          alwaysTryTypes: true,
          project: ['./tsconfig.json', './packages/*/tsconfig.json'],
        },
        node: {
          // pnpm uses symlinks heavily — preserveSymlinks default works,
          // but if you have set NODE_PRESERVE_SYMLINKS=1 you need this:
          extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'],
        },
      },
      'import/internal-regex': '^@my-org/',  // Treat workspace packages as internal
    },
  },
];

Verify the workspace package resolves:

# Each package should appear as a symlink in the consumer's node_modules
ls -la node_modules/@my-org/
# Expected output includes:
# my-package -> ../../packages/my-package

If the symlink is missing, run pnpm install or yarn install at the repo root. A common failure mode is running npm install inside a single workspace package — that breaks the symlink topology and ESLint cannot find the sibling packages.

Yarn PnP (Plug’n’Play) requires the pnp resolver:

npm install --save-dev eslint-import-resolver-node @yarnpkg/pnpify
// .eslintrc.js
module.exports = {
  settings: {
    'import/resolver': {
      node: { extensions: ['.js', '.jsx', '.ts', '.tsx'] },
    },
  },
};

Run ESLint via yarn pnpify --sdk eslint so the resolver can read the .pnp.cjs manifest.

Fix 8: Turn Off import/no-unresolved When Using TypeScript

If you use TypeScript with @typescript-eslint, TypeScript already catches invalid imports. The import/no-unresolved rule becomes redundant and often causes false positives:

// .eslintrc.js — valid approach for TypeScript projects
module.exports = {
  extends: [
    '@typescript-eslint/recommended',
    'plugin:import/recommended',
    'plugin:import/typescript',
  ],
  rules: {
    // TypeScript handles module resolution — disable the duplicate ESLint check
    'import/no-unresolved': 'off',

    // Keep other import rules that TypeScript doesn't cover
    'import/order': ['warn', { alphabetize: { order: 'asc' } }],
    'import/no-duplicates': 'error',
    'import/no-cycle': 'warn',
  },
};

When to disable vs. configure: If your project uses TypeScript strictly (strict: true, no any), disabling import/no-unresolved is reasonable — TypeScript’s moduleResolution catches bad imports at compile time. If you have loose TypeScript settings or JavaScript files, keep the rule and configure the resolver properly.

In Production: Incident Lens

A failing import/no-unresolved rule is build-time noise — until it blocks a deploy. CI pipelines almost universally include eslint --max-warnings 0 as a gate, which means a single false positive halts production releases.

Surface. The PR check turns red. The CI log shows error Unable to resolve path to module '@/components/Foo' import/no-unresolved. The developer who opened the PR reproduces it locally with npm run lint, but their teammate cannot — usually because of differing Node versions, node_modules states, or missing local installs of the resolver. The same code lints clean on one branch and fails on another after a rebase, depending on which tsconfig.json is checked in.

Blast radius. The deploy pipeline is blocked for everyone on that branch. There is no rollback path because the build never produces an artifact. If the rule is configured as error in a shared config, every downstream consumer hits it simultaneously after an upgrade. Hotfix branches off main also fail, which compounds when you are trying to ship an urgent fix.

Alerting. CI status reporting (GitHub Checks, GitLab Pipelines, Buildkite) is the alert channel — there is no runtime metric for a build that never started. Track the trend, not the individual failure: lint failures per week is a useful health signal. A sudden spike usually means someone updated eslint-plugin-import or eslint-import-resolver-typescript without testing the matrix.

Recovery. Inline disable comments (// eslint-disable-next-line import/no-unresolved) unblock the immediate deploy, but they are technical debt. Better short-term fix: pin the resolver to the version that worked, push a corrected tsconfig.json paths block, or downgrade the rule to a warning in the offending package until the underlying resolver mismatch is fixed. For false positives caused by a new ESM-only dependency, add it to the ignore list as a targeted exception:

rules: {
  'import/no-unresolved': ['error', { ignore: ['^pkg-with-broken-exports/'] }],
},

Preventive. Share a single ESLint config across the org via a published package (@my-org/eslint-config), pin it in every repo’s devDependencies, and version it semantically. Run eslint --print-config <file> in CI to detect drift between local and CI environments. Add a pre-commit hook with lint-staged so the failure surfaces before the push. For monorepos, run a verify:workspaces script that installs and lints each package in isolation — catches resolver issues that only show up when a package is consumed standalone.

Still Not Working?

Restart the ESLint language server. ESLint servers in VS Code cache configuration. After changing .eslintrc.js:

  • VS Code: Ctrl+Shift+P → “ESLint: Restart ESLint Server”
  • Or close and reopen the file

Check ESLint version compatibility. eslint-plugin-import v2 works with ESLint v8. For ESLint v9 (flat config), use eslint-plugin-import v2.29+ or the fork eslint-plugin-import-x:

npx eslint --version
npm list eslint-plugin-import

Run ESLint from the command line to see the actual error:

npx eslint src/App.tsx --rule '{"import/no-unresolved": "error"}' --debug 2>&1 | grep -i "resolve\|alias"

Check for missing @types packages:

# If you get import/no-unresolved for a JS package
npm install --save-dev @types/package-name

Check for case sensitivity mismatches. macOS and Windows default to case-insensitive filesystems; Linux is case-sensitive. An import like import Foo from './foo' works locally on macOS but fails on the CI Linux runner with import/no-unresolved. Run git ls-files --eol to inspect exact filenames as Git sees them, and enforce forceConsistentCasingInFileNames: true in tsconfig.json.

Check .npmrc and lockfile drift. If package-lock.json and pnpm-lock.yaml disagree (because two people committed conflicting lockfiles), the installed dependency tree may not match what ESLint expects. Delete node_modules and the relevant lockfile, then reinstall with the canonical package manager for the repo.

Check for outdated eslint-import-resolver-typescript versions. Versions older than 3.0 do not support TypeScript 5+‘s nodenext moduleResolution. Upgrade with npm install --save-dev eslint-import-resolver-typescript@latest and verify with npm list eslint-import-resolver-typescript.

For related ESLint and TypeScript tooling issues, see Fix: ESLint Parsing Error Unexpected Token, Fix: TypeScript Cannot Find Module, Fix: ESLint Flat Config Not Working, and Fix: Vite Failed to Resolve Import.

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