Skip to content

Fix: ESLint Config Not Working — Rules Ignored, Flat Config Errors, or Plugin Not Found

FixDevs · (Updated: )

Part of:  React & Frontend Errors

Quick Answer

How to fix ESLint configuration issues — flat config vs legacy config, extends conflicts, parser options, plugin resolution, per-directory overrides, and migrating to ESLint 9.

The Problem

ESLint rules are defined in the config but have no effect:

// .eslintrc.json
{
  "rules": {
    "no-console": "error"
  }
}
// Running eslint src/ — console.log() passes without error

Or ESLint 9 throws a config error:

Error: Key "extends": Key 0: string is not supported in flat config.
You're using a eslintrc config which is not supported in ESLint v9.

Or a plugin can’t be found despite being installed:

Error: Failed to load plugin 'react' declared in '.eslintrc': Cannot find module 'eslint-plugin-react'

Or TypeScript-specific rules don’t apply to .ts files.

Why This Happens

ESLint has two incompatible config systems that are easy to mix up. The legacy .eslintrc.* system (cascading config files, extends as an array of strings, plugin names as bare strings) was the standard from 2013 through 2024. The flat config system (eslint.config.js, plugins imported as JavaScript modules, a single config array with explicit files: matchers) was introduced experimentally in ESLint 8.21 (August 2022) and became the default in ESLint 9 (April 2024). Many tutorials, plugin READMEs, and Stack Overflow answers still target the legacy system.

The flat config is not just a cosmetic redesign. The two systems resolve plugins differently, handle file matching differently, and treat ignores differently. Mixing them in the same project — even by accident, through a transitive plugin that ships a flat config file — causes silent rule loss because the legacy CLI ignores eslint.config.js and the flat CLI ignores .eslintrc.*. The diagnostic question is always: which config did ESLint actually load? Use eslint --print-config <file> to find out, not the file you think it should have loaded.

Plugin resolution is the second large source of confusion. In legacy config, "plugins": ["react"] instructs ESLint to look up the package eslint-plugin-react in node_modules, using a resolution algorithm specific to ESLint. In flat config, you write import reactPlugin from 'eslint-plugin-react' and assign it to a key in plugins: {}. The key is the namespace used in rule IDs (react/jsx-uses-react), and it can be anything — there is no automatic naming convention. Mistype the key and rules silently do nothing.

  • ESLint 9 uses flat config by defaulteslint.config.js (or .mjs/.cjs). The old extends, .eslintrc.* format, and eslintignore are no longer supported without a compatibility layer.
  • ESLint 8 and below use legacy config.eslintrc.json, .eslintrc.js, .eslintrc.yaml. extends is an array of strings. This format is deprecated.
  • Plugin resolution changed in flat config — in legacy config, plugins are referenced as strings ("plugin:react/recommended"). In flat config, you import them as JavaScript modules.
  • File matching — rules in overrides or flat config files glob patterns must match your file paths exactly. A pattern that doesn’t match silently skips the file.

Fix 1: Determine Which Config System You’re Using

# Check your ESLint version
npx eslint --version
# v8.x.x → legacy config (.eslintrc.*)
# v9.x.x → flat config (eslint.config.js) by default

# Check which config file ESLint found
npx eslint --print-config src/index.js
# Shows the resolved config for that file
# If it shows empty rules, the config isn't being applied

# Lint a specific file with debug output
npx eslint --debug src/index.js 2>&1 | grep -E "config|plugin"

ESLint 9 with legacy config compatibility:

// eslint.config.js — use FlatCompat to use old configs in ESLint 9
import { FlatCompat } from '@eslint/eslintrc';
import path from 'path';
import { fileURLToPath } from 'url';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const compat = new FlatCompat({ baseDirectory: __dirname });

export default [
  // Convert legacy configs
  ...compat.extends('eslint:recommended'),
  ...compat.extends('plugin:react/recommended'),

  // Add flat config rules on top
  {
    rules: {
      'no-console': 'warn',
    },
  },
];

Fix 2: Write a Correct ESLint 9 Flat Config

If you’re on ESLint 9, use the flat config format natively:

// eslint.config.js (ESLint 9 flat config)
import js from '@eslint/js';
import globals from 'globals';

export default [
  // Apply recommended rules to all JS files
  js.configs.recommended,

  {
    // Files this config applies to
    files: ['**/*.{js,mjs,cjs}'],

    // Global variables available
    languageOptions: {
      globals: {
        ...globals.browser,
        ...globals.node,
      },
      ecmaVersion: 2022,
      sourceType: 'module',
    },

    rules: {
      'no-console': 'warn',
      'no-unused-vars': 'error',
      eqeqeq: ['error', 'always'],
    },
  },

  // Ignore patterns (replaces .eslintignore)
  {
    ignores: ['dist/**', 'node_modules/**', '*.min.js'],
  },
];

ESLint 9 with TypeScript:

// eslint.config.js
import js from '@eslint/js';
import tseslint from 'typescript-eslint';
import globals from 'globals';

export default tseslint.config(
  js.configs.recommended,
  ...tseslint.configs.recommended,
  {
    files: ['**/*.{ts,tsx}'],
    languageOptions: {
      globals: globals.browser,
      parserOptions: {
        project: './tsconfig.json',  // Required for type-aware rules
      },
    },
    rules: {
      '@typescript-eslint/no-explicit-any': 'warn',
      '@typescript-eslint/explicit-function-return-type': 'off',
    },
  },
  {
    // Different rules for test files
    files: ['**/*.test.{ts,tsx}', '**/*.spec.{ts,tsx}'],
    rules: {
      '@typescript-eslint/no-explicit-any': 'off',
    },
  },
);

Fix 3: Fix Legacy Config (ESLint 8 and Below)

If you’re on ESLint 8 and using .eslintrc.*:

// .eslintrc.json — legacy format
{
  "env": {
    "browser": true,
    "es2022": true,
    "node": true
  },
  "extends": [
    "eslint:recommended",
    "plugin:react/recommended",
    "plugin:@typescript-eslint/recommended"
  ],
  "parser": "@typescript-eslint/parser",
  "parserOptions": {
    "ecmaVersion": "latest",
    "sourceType": "module",
    "ecmaFeatures": {
      "jsx": true
    }
  },
  "plugins": ["react", "@typescript-eslint"],
  "rules": {
    "no-console": "warn",
    "react/prop-types": "off"
  },
  "overrides": [
    {
      // TypeScript-specific rules
      "files": ["**/*.ts", "**/*.tsx"],
      "rules": {
        "@typescript-eslint/no-explicit-any": "error"
      }
    },
    {
      // Relax rules in test files
      "files": ["**/*.test.*", "**/*.spec.*", "tests/**/*"],
      "rules": {
        "no-console": "off",
        "@typescript-eslint/no-explicit-any": "off"
      }
    }
  ],
  "ignorePatterns": ["dist/", "node_modules/", "*.config.js"]
}

Verify the extends order — later entries override earlier ones:

{
  "extends": [
    "eslint:recommended",
    "plugin:react/recommended",   // Adds React rules
    "prettier"                    // Must be LAST — disables formatting rules
  ]
}

Fix 4: Fix Plugin Resolution Errors

# Plugin not found — check it's actually installed
npm ls eslint-plugin-react
# If not listed: install it
npm install --save-dev eslint-plugin-react

# Verify the package resolves
node -e "require('eslint-plugin-react')"

# In monorepos, plugins must be in the root node_modules
# (not just in the package's node_modules)
npm install --save-dev eslint-plugin-react -w .  # Install at workspace root

Plugin name mismatch in flat config:

// In legacy config, plugins are referenced as strings:
// "plugins": ["react"]  → loads eslint-plugin-react

// In flat config, you import the module directly:
import reactPlugin from 'eslint-plugin-react';

export default [
  {
    plugins: {
      react: reactPlugin,  // 'react' is the namespace in rules
    },
    rules: {
      'react/jsx-uses-react': 'error',  // Namespace matches key above
    },
  },
];

Common plugin imports for flat config:

import js from '@eslint/js';
import tseslint from 'typescript-eslint';
import reactPlugin from 'eslint-plugin-react';
import reactHooksPlugin from 'eslint-plugin-react-hooks';
import importPlugin from 'eslint-plugin-import';
import prettierConfig from 'eslint-config-prettier';

export default [
  js.configs.recommended,
  ...tseslint.configs.recommended,
  {
    plugins: {
      react: reactPlugin,
      'react-hooks': reactHooksPlugin,
      import: importPlugin,
    },
    rules: {
      ...reactPlugin.configs.recommended.rules,
      ...reactHooksPlugin.configs.recommended.rules,
      'import/no-unresolved': 'error',
    },
  },
  prettierConfig,  // Disable formatting rules (must be last)
];

Fix 5: Fix Rules Not Applying to Certain Files

If rules work in some files but not others, the issue is usually file matching:

// eslint.config.js — check files glob patterns
export default [
  {
    files: ['src/**/*.js'],  // Only applies to .js in src/
    rules: { 'no-console': 'error' },
  },
  // If you have .mjs or .cjs files, they won't match '**/*.js'
  {
    files: ['**/*.{js,mjs,cjs}'],  // More inclusive pattern
    rules: { 'no-console': 'error' },
  },
];

Debug which config applies to a file:

# See the full resolved config for a specific file
npx eslint --print-config src/components/Button.tsx

# Check if a file is being ignored
npx eslint --debug src/components/Button.tsx 2>&1 | grep "Skipping"

# List all files ESLint will lint
npx eslint --print-config --debug . 2>&1 | grep "Linting:"

Common file matching mistakes:

// WRONG — doesn't match .tsx files
files: ['**/*.ts']

// CORRECT — matches both .ts and .tsx
files: ['**/*.{ts,tsx}']

// WRONG — doesn't match files in subdirectories
files: ['src/*.ts']

// CORRECT — matches recursively
files: ['src/**/*.ts']

// WRONG — relative path issue in some setups
files: ['./src/**/*.ts']  // Leading './' may cause issues

// CORRECT
files: ['src/**/*.ts']

Fix 6: Set Up ESLint with VS Code

Common VS Code ESLint extension issues:

// .vscode/settings.json
{
  // Enable ESLint for these file types
  "eslint.validate": [
    "javascript",
    "javascriptreact",
    "typescript",
    "typescriptreact"
  ],

  // Fix on save
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": "explicit"
  },

  // Use the local ESLint (not global)
  "eslint.useFlatConfig": true,  // For ESLint 9 flat config

  // Show ESLint status in status bar
  "eslint.alwaysShowStatus": true
}

If the extension shows “ESLint server is not running”:

# Check ESLint is installed locally
npm ls eslint

# Check the ESLint output panel in VS Code
# View → Output → ESLint

# Common fix: the extension uses the wrong ESLint version
# Add to settings.json:
# "eslint.nodePath": "./node_modules"

Version History: ESLint Config Across Major Versions

ESLint’s config system has gone through three eras. Understanding which era a tutorial or plugin was written for is the difference between a five-minute fix and an afternoon of debugging.

ESLint 0.x through 5.x (2013-2018) was the original .eslintrc.* cascade. Configs cascaded up the directory tree, root: true stopped the search, and extends accepted strings (shareable configs) or arrays of strings. The model was familiar to anyone who had used .editorconfig or tsconfig.json, but it had subtle bugs around plugin resolution in monorepos that were never fully fixed.

ESLint 6 (June 2019) dropped Node 6 support and made plugin resolution stricter — plugins had to be in the same node_modules tree as the ESLint binary, which broke many monorepo setups overnight. The 6.7 release added per-config plugin namespacing as a workaround but the underlying friction never went away. This is the era of --resolve-plugins-relative-to workarounds and “why doesn’t my plugin load” Stack Overflow questions.

ESLint 7 (May 2020) dropped Node 8 and required Node 10. The legacy config system was already showing its age — async rule discovery, parallelism, and cross-cutting overrides all hit limits — and the maintainers started prototyping flat config publicly. ESLint 7 is still functional and is the last version many older codebases pinned to before the great flat config migration.

ESLint 8 (October 2021) dropped Node 10 and required Node 12.22. The 8.x line is the bridge release. Flat config landed experimentally in 8.21 (August 2022) behind the ESLINT_USE_FLAT_CONFIG=true environment variable. Plugin authors had a runway from 8.21 onward to ship dual configs (one for legacy, one for flat). The @eslint/eslintrc compat layer shown in Fix 1 was released alongside 8.21 specifically to bridge the gap.

ESLint 9 (April 2024) made flat config the default and removed many legacy formatters and rules. Codebases that did not migrate before 9 saw the “string is not supported in flat config” error on first install. The migration path is: stay on 8.57 (the final 8.x release) while migrating, or jump to 9 with FlatCompat as a temporary shim. Many shops are still on 8.57 in 2026 because flat config migration cost more than expected.

typescript-eslint 7 (March 2024) released full flat config support. Versions 5 and 6 supported flat config only through FlatCompat or hand-rolled wrappers; version 7 introduced the tseslint.config() helper shown in Fix 2 that handles the boilerplate. If you are on typescript-eslint 5 or 6 with ESLint 9, the cleanest upgrade is to bump both at once rather than nurse a half-migrated state.

Major plugin migration timeline:

  • eslint-plugin-react shipped flat config support in v7.34 (April 2024)
  • eslint-plugin-react-hooks shipped flat support in v5.0 (October 2024)
  • eslint-plugin-import shipped flat support in v2.30 (September 2024); the maintenance has since forked into eslint-plugin-import-x for active development
  • eslint-config-prettier v9 (December 2023) works as a flat config object without wrapping
  • eslint-plugin-jsx-a11y shipped flat support in v6.10 (October 2024)
  • eslint-plugin-vue shipped flat support in v9.27 (June 2024)

If a plugin you depend on is older than its flat-config-shipping version, FlatCompat is the bridge — but ideally upgrade to the version that ships flat configs natively, because the compat layer adds startup overhead and occasionally misinterprets rule severity.

Node.js requirement progression matters for CI pinning:

  • ESLint 7: Node 10.12+
  • ESLint 8: Node 12.22+ or 14.17+ or 16+
  • ESLint 9: Node 18.18+ or 20.9+ or 21.1+

Pin your CI Node version to match. ESLint 9 will refuse to run on Node 16 and below, even if package.json says it should work.

Still Not Working?

Multiple config files — ESLint picks only one — in legacy config mode, ESLint uses the closest config file to the linted file. If you have an .eslintrc.json in a subdirectory, it overrides the root config for that directory. Use root: true in the root config to stop ESLint from searching further up:

// Root .eslintrc.json
{
  "root": true,
  "rules": { ... }
}

eslint-config-prettier must be lasteslint-config-prettier disables all formatting-related ESLint rules to avoid conflicts with Prettier. It must be the last item in extends (legacy) or the last item in the flat config array. Placing it earlier means a later config re-enables the formatting rules.

Rules from extends not applying — if you override a rule in rules with "off", it disables the rule even if extends enables it. Check if you’ve accidentally disabled a rule you want to keep.

eslint.useFlatConfig mismatch in VS Code — the VS Code ESLint extension has its own opinion about whether to use flat config. If your project is on ESLint 9 (flat config by default) but the extension is on a pre-2.4 version, the extension may still try the legacy resolution path and fail silently. Upgrade the extension and set "eslint.useFlatConfig": true explicitly to remove ambiguity. ESLint 9.10+ no longer needs the setting on recent extension versions, but explicit is safer.

Monorepo: plugins resolve from the wrong workspace — pnpm and Yarn workspaces hoist some packages and isolate others. ESLint resolves plugins using Node’s require.resolve starting from the config file’s directory. If eslint.config.js is at the repo root but the workspace package has its own node_modules, the plugin may be loaded from the workspace, not the root, and the version that ends up running is whichever was installed first. Use npm ls eslint-plugin-react (or the equivalent for your package manager) at each level to confirm.

.eslintignore is silently ignored in flat config — flat config replaces .eslintignore with an explicit ignores: [...] array entry in eslint.config.js. If you migrate the config but leave the .eslintignore file in place, it does nothing and the patterns inside become live again. Delete the file after migrating its contents into the config.

For related ESLint issues, see Fix: ESLint Import No Unresolved, Fix: ESLint Parsing Error Unexpected Token, Fix: ESLint Flat Config Not Working, and Fix: TypeScript Cannot Find Module.

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