Skip to main content

Migrate to ESLint v10

ESLint v10.0.0 is a major release with several breaking changes. This guide walks you through upgrading from v9.x to v10.x, with real examples and actionable steps.
ESLint v10 was released on February 6, 2026 and requires Node.js ^20.19.0 || ^22.13.0 || >=24.

Quick Migration Checklist

1

Update Node.js

Ensure you’re running Node.js v20.19.0 or later:
node --version
If your version is older, upgrade to at least Node.js v20.19.0.
2

Update ESLint

Update ESLint to v10.x:
npm install eslint@latest --save-dev
3

Migrate Configuration

Remove any eslintrc files and ensure you’re using flat config (eslint.config.js).
4

Remove Feature Flags

Remove the v10_config_lookup_from_file flag from your configuration:
# Remove from CLI
--flag v10_config_lookup_from_file

# Remove from environment
ESLINT_FLAGS="v10_config_lookup_from_file"
5

Test Your Configuration

Run ESLint to verify everything works:
npx eslint .

Breaking Changes

Node.js Version Requirements

Breaking Change: ESLint v10 drops support for Node.js versions < v20.19, v21, and v23.
ESLint v10 requires:
  • Node.js ^20.19.0
  • Node.js ^22.13.0
  • Node.js >=24
Migration:
# Check your Node.js version
node --version

# If < v20.19.0, upgrade Node.js
nvm install 20.19.0
nvm use 20.19.0
If you cannot upgrade Node.js immediately, continue using ESLint v9.x until you can upgrade.

Configuration File Lookup

Breaking Change: Config file lookup now starts from the linted file’s directory, not the current working directory.
In v9, ESLint looked for eslint.config.js starting from your current working directory. In v10, it starts from each linted file’s directory and searches upward. Before (v9 with CWD-based lookup):
project/
  ├── eslint.config.js    ← Always used
  ├── src/
  │   └── app.js
  └── tests/
      └── test.js
After (v10 with file-based lookup):
project/
  ├── eslint.config.js    ← Used for all files
  ├── src/
  │   ├── eslint.config.js ← Used only for src/ files (if exists)
  │   └── app.js
  └── tests/
      └── test.js
Migration: If you relied on the old behavior, explicitly specify your config file:
npx eslint --config ./eslint.config.js src/

ESLintrc Format Removed

Breaking Change: The legacy .eslintrc configuration format is no longer supported.
ESLint v10 only supports the flat config format (eslint.config.js). Old Format (.eslintrc.json):
{
  "extends": ["eslint:recommended"],
  "rules": {
    "no-unused-vars": "error"
  },
  "env": {
    "node": true
  }
}
New Format (eslint.config.js):
import js from "@eslint/js";

export default [
  js.configs.recommended,
  {
    files: ["**/*.js"],
    languageOptions: {
      globals: {
        ...globalThis,
        process: "readonly",
        console: "readonly"
      }
    },
    rules: {
      "no-unused-vars": "error"
    }
  }
];
Remove These:
  • .eslintrc
  • .eslintrc.json
  • .eslintrc.js
  • .eslintrc.yml
  • eslintConfig in package.json
  • ESLINT_USE_FLAT_CONFIG environment variable
See the Configuration Migration Guide for detailed conversion steps.

JSX Reference Tracking

Enhancement: ESLint now correctly tracks JSX references in scope analysis.
Previously, ESLint didn’t recognize JSX identifiers as references: Example:
import { Card } from "./card.jsx";

export function createCard(name) {
  return <Card name={name} />; // v9: Card not recognized as "used"
}
Before v10:
  • no-unused-vars would report Card as unused ❌
  • Removing the import wouldn’t trigger no-undef
After v10:
  • <Card> is correctly recognized as a reference to Card
  • Scope analysis works correctly for JSX ✅
Migration:
  1. Remove workaround rules like @eslint-react/jsx-uses-vars:
// Remove this from your config
import eslintReact from "@eslint-react/eslint-plugin";

export default [
  {
    rules: {
      "@eslint-react/jsx-uses-vars": "error" // ❌ No longer needed
    }
  }
];
  1. Fix new linting errors that may appear in JSX files

eslint-env Comments Now Error

Breaking Change: /* eslint-env */ comments now cause linting errors.
This will now fail:
/* eslint-env node */
const fs = require('fs');
Error message:
error: /* eslint-env */ comments are no longer supported at file.js:1:1:
> 1 | /* eslint-env node */
    | ^
Migration: Remove eslint-env comments and configure globals in eslint.config.js:
import globals from "globals";

export default [
  {
    languageOptions: {
      globals: {
        ...globals.node
      }
    }
  }
];

Updated eslint:recommended

Three new rules are enabled in eslint:recommended.
New rules in v10:
  • no-unassigned-vars - Disallow variables that are never assigned
  • no-useless-assignment - Disallow assignments with no effect
  • preserve-caught-error - Require catch parameters to be used
Example violations:
// no-unassigned-vars
let x; // ❌ Never assigned
console.log(x);

// no-useless-assignment  
let y = 1;
y = 2; // ❌ Overwritten before use
y = 3;
console.log(y);

// preserve-caught-error
try {
  riskyOperation();
} catch (err) { // ❌ Parameter not used
  console.log("Error occurred");
}
To disable:
export default [
  {
    rules: {
      "no-unassigned-vars": "off",
      "no-useless-assignment": "off",
      "preserve-caught-error": "off"
    }
  }
];

API Changes for Plugin Developers

RuleTester Stricter Validation

Valid test cases cannot have errors or output properties.
This will now fail:
ruleTester.run("my-rule", rule, {
  valid: [
    {
      code: "const x = 1;",
      errors: 0,  // ❌ Not allowed
      output: "const x = 1;" // ❌ Not allowed
    }
  ],
  invalid: []
});
Correct usage:
ruleTester.run("my-rule", rule, {
  valid: [
    "const x = 1;" // ✅ Simple string
  ],
  invalid: [
    {
      code: "var x = 1;",
      output: "const x = 1;",
      errors: [{ message: "Use const" }]
    }
  ]
});

Removed context Methods

Deprecated context methods have been removed.
Migration table:
RemovedReplacement
context.getCwd()context.cwd
context.getFilename()context.filename
context.getPhysicalFilename()context.physicalFilename
context.getSourceCode()context.sourceCode
context.parserOptionscontext.languageOptions
Example migration:
// Before
module.exports = {
  create(context) {
    const filename = context.getFilename();
    const sourceCode = context.getSourceCode();
    const cwd = context.getCwd();
    
    // ...
  }
};

// After
module.exports = {
  create(context) {
    const filename = context.filename;
    const sourceCode = context.sourceCode;
    const cwd = context.cwd;
    
    // ...
  }
};
Use the eslint-transforms utility to automate this migration:
npm install -g eslint-transforms
eslint-transforms v9-rule-migration rules/

Removed SourceCode Methods

Deprecated SourceCode methods have been removed.
RemovedReplacement
getTokenOrCommentBefore(node, skip)getTokenBefore(node, { includeComments: true, skip })
getTokenOrCommentAfter(node, skip)getTokenAfter(node, { includeComments: true, skip })
isSpaceBetweenTokens(first, second)isSpaceBetween(first, second)
getJSDocComment(node)No replacement
Example:
// Before
const token = sourceCode.getTokenOrCommentBefore(node, 1);
const hasSpace = sourceCode.isSpaceBetweenTokens(first, second);

// After
const token = sourceCode.getTokenBefore(node, { includeComments: true, skip: 1 });
const hasSpace = sourceCode.isSpaceBetween(first, second);

Dependency Updates

Jiti Version Requirement

If using TypeScript config files, ensure jiti is at least v2.2.0.
npm install jiti@^2.2.0 --save-dev

Minimatch v10

ESLint v10 uses minimatch v10, which supports POSIX character classes:
# Match files starting with uppercase letters
npx eslint "**/[[:upper:]]*.js"

# Match files with digits
npx eslint "**/[[:digit:]]*.js"

Common Migration Issues

Solution: Remove the flag from your configuration. It’s now the default behavior.
# Remove this flag
--flag v10_config_lookup_from_file
Solution: Ensure you have eslint.config.js (not .eslintrc) in your project root.
# Create a basic config
npm init @eslint/config@latest
Solution: Remove eslint-env comments and configure globals in eslint.config.js:
import globals from "globals";

export default [{
  languageOptions: {
    globals: globals.browser
  }
}];
Solution: This shouldn’t happen in v10 (JSX tracking is fixed). If you see this, ensure you’ve removed workaround plugins like @eslint-react/jsx-uses-vars.
Solution: Update your rule to use context.filename instead of context.getFilename():
// Before
const filename = context.getFilename();

// After
const filename = context.filename;

Testing Your Migration

1

Run ESLint

npx eslint .
2

Check for new errors

Review any new errors from eslint:recommended rules.
3

Test in CI/CD

Ensure your CI/CD pipeline uses Node.js v20.19+ and ESLint v10.
4

Update editor integrations

Restart your editor to pick up the new ESLint version.

Resources

Official Migration Guide

Complete official migration documentation

Configuration Guide

Learn about flat config format

Breaking Changes

Full list of breaking changes

CHANGELOG

Complete version history

Getting Help

If you encounter issues during migration:

Build docs developers (and LLMs) love