Versions

Migrate to v9.x

ESLint v9.0.0 is a major release of ESLint, and as such, has several breaking changes that you need to be aware of. This guide is intended to walk you through the breaking changes.

The lists below are ordered roughly by the number of users each change is expected to affect, where the first items are expected to affect the most users.

Table of Contents

Breaking changes for users

Breaking changes for plugin developers

Breaking changes for integration developers


Node.js < v18.18, v19 are no longer supported

ESLint is officially dropping support for these versions of Node.js starting with ESLint v9.0.0. ESLint now supports the following versions of Node.js:

  • Node.js v18.18.0 and above
  • Node.js v20.9.0 and above
  • Node.js v21 and above

To address: Make sure you upgrade to at least Node.js v18.18.0 when using ESLint v9.0.0. One important thing to double check is the Node.js version supported by your editor when using ESLint via editor integrations. If you are unable to upgrade, we recommend continuing to use ESLint v8.56.0 until you are able to upgrade Node.js.

Related issue(s): #17595

New default config format (eslint.config.js)

As announced in our blog post, in ESLint v9.0.0, eslint.config.js is the new default configuration format. The previous format, eslintrc, is now deprecated and will not automatically be searched for.

To address: Update your configuration to the new format following the Configuration Migration Guide. In case you still need to use the deprecated eslintrc config format, set environment variable ESLINT_USE_FLAT_CONFIG to false.

Related Issues(s): #13481

Removed multiple formatters

ESLint v9.0.0 has removed the following formatters from the core:

Removed Formatter Replacement npm Package
checkstyle eslint-formatter-checkstyle
compact eslint-formatter-compact
jslint-xml eslint-formatter-jslint-xml
junit eslint-formatter-junit
tap eslint-formatter-tap
unix eslint-formatter-unix
visualstudio eslint-formatter-visualstudio

To address: If you are using any of these formatters via the -f command line flag, you’ll need to install the respective package for the formatter.

Related issue(s): #17524

Removed require-jsdoc and valid-jsdoc rules

The require-jsdoc and valid-jsdoc rules have been removed in ESLint v9.0.0. These rules were initially deprecated in 2018.

To address: Use the replacement rules in eslint-plugin-jsdoc.

Related issue(s): #15820

eslint:recommended has been updated

Four new rules have been enabled in eslint:recommended:

Additionally, the following rules have been removed from eslint:recommended:

To address: Fix errors or disable these rules.

Related issue(s): #15576, #17446, #17596

--quiet no longer runs rules set to "warn"

Prior to ESLint v9.0.0, the --quiet CLI flag would run all rules set to either "error" or "warn" and then hide the results from rules set to "warn". In ESLint v9.0.0, --quiet will prevent rules from being executed when set to "warn". This can result in a performance improvement for configurations containing many rules set to "warn".

If --max-warnings is used then --quiet will not suppress the execution of rules set to "warn" but the output of those rules will be suppressed.

To address: In most cases, this change is transparent. If, however, you are running a rule set to "warn" that makes changes to the data available to other rules (for example, if the rule uses sourceCode.markVariableAsUsed()), then this can result in a behavior change. In such a case, you’ll need to either set the rule to "error" or stop using --quiet.

Related issue(s): #16450

--output-file now writes a file to disk even with an empty output

Prior to ESLint v9.0.0, the --output-file flag would skip writing a file to disk if the output was empty. However, in ESLint v9.0.0, --output-file now consistently writes a file to disk, even when the output is empty. This update ensures a more consistent and reliable behavior for --output-file.

To address: Review your usage of the --output-file flag, especially if your processes depend on the file’s presence or absence based on output content. If necessary, update your scripts or configurations to accommodate this change.

Related Issues(s): #17660

Change in behavior when no patterns are passed to CLI

Prior to ESLint v9.0.0, running the ESLint CLI without any file or directory patterns would result in no files being linted and would exit with code 0. This was confusing because it wasn’t clear that nothing had actually happened. In ESLint v9.0.0, this behavior has been updated:

  • Flat config. If you are using flat config, you can run npx eslint or eslint (if globally installed) and ESLint will assume you want to lint the current directory. Effectively, passing no patterns is equivalent to passing ..
  • eslintrc. If you are using the deprecated eslintrc config, you’ll now receive an error when running the CLI without any patterns.

To address: In most cases, no change is necessary, and you may find some locations where you thought ESLint was running but it wasn’t. If you’d like to keep the v8.x behavior, where passing no patterns results in ESLint exiting with code 0, add the --pass-on-no-patterns flag to the CLI call.

Related issue(s): #14308

/* eslint */ comments with only severity now retain options from the config file

Prior to ESLint v9.0.0, configuration comments such as /* eslint curly: "warn" */ or /* eslint curly: ["warn"] */ would completely override any configuration specified for the rule in the config file, and thus enforce the default options of the rule.

In ESLint v9.0.0, the behavior of configuration comments is aligned with how rule configurations in config files are merged, meaning that a configuration comment with only severity now retains options specified in the config file and just overrides the severity.

For example, if you have the following config file:

// eslint.config.js

export default [{
    rules: {
        curly: ["error", "multi"]
    }
}];

and the following configuration comment:

// my-file.js

/* eslint curly: "warn" */

the resulting configuration for the curly rule when linting my-file.js will be curly: ["warn", "multi"].

Note that this change only affects cases where the same rule is configured in the config file with options and using a configuration comment without options. In all other cases (e.g. the rule is only configured using a configuration comment), the behavior remains the same as prior to ESLint v9.0.0.

To address: We expect that in most cases no change is necessary, as rules configured using configuration comments are typically not already configured in the config file. However, if you need a configuration comment to completely override configuration from the config file and enforce the default options, you’ll need to specify at least one option:

// my-file.js

/* eslint curly: ["warn", "all"] */

Related issue(s): #17381

Stricter /* exported */ parsing

Prior to ESLint v9.0.0, the /* exported */ directive incorrectly allowed the following syntax:

/* exported foo: true, bar: false */

// and

/* exported foo bar */

The true and false in this example had no effect on ESLint’s behavior, and in fact, was a parsing bug.

In ESLint v9.0.0, any /* exported */ variables followed by a colon and value will be ignored as invalid.

To address: Update any /* exported */ directives to eliminate the colons and subsequent values, and ensure there are commas between variable names such as:

/* exported foo, bar */

Related issue(s): #17622

no-constructor-return and no-sequences rule schemas are stricter

In previous versions of ESLint, no-constructor-return and no-sequences rules were mistakenly accepting invalid options.

This has been fixed in ESLint v9.0.0:

  • The no-constructor-return rule does not accept any options.
  • The no-sequences rule can take one option, an object with a property "allowInParentheses" (boolean).
{
    "rules": {
        "no-constructor-return": ["error"],
        "no-sequences": ["error", { "allowInParentheses": false }]
    }
}

To address: If ESLint reports invalid configuration for any of these rules, update your configuration.

Related issue(s): #16879

New checks in no-implicit-coercion by default

In ESLint v9.0.0, the no-implicit-coercion rule additionally reports the following cases by default:

-(-foo);
foo - 0;

To address: If you want to retain the previous behavior of this rule, set "allow": ["-", "- -"].

{
    "rules": {
        "no-implicit-coercion": [2, { "allow": ["-", "- -"] } ],
    }
}

Related issue(s): #17832

Case-sensitive flags in no-invalid-regexp

In ESLint v9.0.0, the option allowConstructorFlags is now case-sensitive.

To address: Update your configuration if needed.

Related issue(s): #16574

varsIgnorePattern option of no-unused-vars no longer applies to catch arguments

In previous versions of ESLint, the varsIgnorePattern option of no-unused-vars incorrectly ignored errors specified in a catch clause. In ESLint v9.0.0, varsIgnorePattern no longer applies to errors in catch clauses. For example:

/*eslint no-unused-vars: ["error", { "caughtErrors": "all", "varsIgnorePattern": "^err" }]*/

try {
    //...
} catch (err) { // 'err' will be reported.
    console.error("errors");
}

To address: If you want to specify ignore patterns for catch clause variable names, use the caughtErrorsIgnorePattern option in addition to varsIgnorePattern.

Related issue(s): #17540

no-restricted-imports now accepts multiple config entries with the same name

In previous versions of ESLint, if multiple entries in the paths array of your configuration for the no-restricted-imports rule had the same name property, only the last one would apply, while the previous ones would be ignored.

As of ESLint v9.0.0, all entries apply, allowing for specifying different messages for different imported names. For example, you can now configure the rule like this:

{
    rules: {
        "no-restricted-imports": ["error", {
            paths: [
                {
                    name: "react-native",
                    importNames: ["Text"],
                    message: "import 'Text' from 'ui/_components' instead"
                },
                {
                    name: "react-native",
                    importNames: ["View"],
                    message: "import 'View' from 'ui/_components' instead"
                }
            ]
        }]
    }
}

and both import { Text } from "react-native" and import { View } from "react-native" will be reported, with different messages.

In previous versions of ESLint, with this configuration only import { View } from "react-native" would be reported.

To address: If your configuration for this rule has multiple entries with the same name, you may need to remove unintentional ones.

Related issue(s): #15261

"eslint:recommended" and "eslint:all" no longer accepted in flat config

In ESLint v8.x, eslint.config.js could refer to "eslint:recommended" and "eslint:all" configurations by inserting a string into the config array, as in this example:

// eslint.config.js
export default [
    "eslint:recommended",
    "eslint:all"
];

In ESLint v9.0.0, this format is no longer supported and will result in an error.

To address: Use the @eslint/js package instead:

// eslint.config.js
import js from "@eslint/js";

export default [
    js.configs.recommended,
    js.configs.all
];

Related issue(s): #17488

no-inner-declarations has a new default behavior with a new option

ESLint v9.0.0 introduces a new option in no-inner-declarations rule called blockScopeFunctions which by default allows block-level functions in strict mode when languageOptions.ecmaVersion is set to 2015 or above.

/*eslint no-inner-declarations: "error"*/
"use strict";

if (test) {
    function foo () { }  // no error
}

To address: If you want to report the block-level functions in every condition regardless of strict or non-strict mode, set the blockScopeFunctions option to "disallow".

Related issue(s): #15576

no-unused-vars now defaults caughtErrors to "all"

ESLint v9.0.0 changes the default value for the no-unused-vars rule’s caughtErrors option. Previously it defaulted to "none" to never check whether caught errors were used. It now defaults to "all" to check caught errors for being used.

/*eslint no-unused-vars: "error"*/
try {}
catch (error) {
    // 'error' is defined but never used
}

To address: If you want to allow unused caught errors, such as when writing code that will be directly run in an environment that does not support ES2019 optional catch bindings, set the caughtErrors option to "none". Otherwise, delete the unused caught errors.

/*eslint no-unused-vars: "error"*/
try {}
catch {
    // no error
}

Related issue(s): #17974

no-useless-computed-key flags unnecessary computed member names in classes by default

In ESLint v9.0.0, the default value of the enforceForClassMembers option of the no-useless-computed-key rule was changed from false to true. The effect of this change is that unnecessary computed member names in classes will be flagged by default.

/*eslint no-useless-computed-key: "error"*/

class SomeClass {
    ["someMethod"]() {} // ok in ESLint v8, error in ESLint v9.
}

To address: Fix the problems reported by the rule or revert to the previous behavior by setting the enforceForClassMembers option to false.

Related issue(s): #18042

Removed multiple context methods

ESLint v9.0.0 removes multiple deprecated methods from the context object and moves them onto the SourceCode object:

Removed on context Replacement(s) on SourceCode
context.getSource() sourceCode.getText()
context.getSourceLines() sourceCode.getLines()
context.getAllComments() sourceCode.getAllComments()
context.getNodeByRangeIndex() sourceCode.getNodeByRangeIndex()
context.getComments() sourceCode.getCommentsBefore(), sourceCode.getCommentsAfter(), sourceCode.getCommentsInside()
context.getCommentsBefore() sourceCode.getCommentsBefore()
context.getCommentsAfter() sourceCode.getCommentsAfter()
context.getCommentsInside() sourceCode.getCommentsInside()
context.getJSDocComment() sourceCode.getJSDocComment()
context.getFirstToken() sourceCode.getFirstToken()
context.getFirstTokens() sourceCode.getFirstTokens()
context.getLastToken() sourceCode.getLastToken()
context.getLastTokens() sourceCode.getLastTokens()
context.getTokenAfter() sourceCode.getTokenAfter()
context.getTokenBefore() sourceCode.getTokenBefore()
context.getTokenByRangeStart() sourceCode.getTokenByRangeStart()
context.getTokens() sourceCode.getTokens()
context.getTokensAfter() sourceCode.getTokensAfter()
context.getTokensBefore() sourceCode.getTokensBefore()
context.getTokensBetween() sourceCode.getTokensBetween()
context.parserServices sourceCode.parserServices
context.getDeclaredVariables() sourceCode.getDeclaredVariables()

In addition to the methods in the above table, there are several other methods that are also moved but required different method signatures:

Removed on context Replacement(s) on SourceCode
context.getAncestors() sourceCode.getAncestors(node)
context.getScope() sourceCode.getScope(node)
context.markVariableAsUsed(name) sourceCode.markVariableAsUsed(name, node)

To address: Following the recommendations in the blog post.

Related Issues(s): #16999, #13481

Removed sourceCode.getComments()

ESLint v9.0.0 removes the deprecated sourceCode.getComments() method.

To address: Replace with sourceCode.getCommentsBefore(), sourceCode.getCommentsAfter(), or sourceCode.getCommentsInside().

Related Issues(s): #14744

Removed CodePath#currentSegments

ESLint v9.0.0 removes the deprecated CodePath#currentSegments property.

To address: Update your code following the recommendations in the blog post.

Related Issues(s): #17457

Function-style rules are no longer supported

ESLint v9.0.0 drops support for function-style rules. Function-style rules are rules created by exporting a function rather than an object with a create() method. This rule format was deprecated in 2016.

To address: Update your rules to the most recent rule format.

The eslint-plugin/prefer-object-rule rule can help enforce the usage of object-style rules and autofix any remaining function-style rules.

Related Issues(s): #14709

meta.schema is required for rules with options

As of ESLint v9.0.0, an error will be thrown if any options are passed to a rule that doesn’t specify meta.schema property.

To address:

  • If your rule expects options, set meta.schema property to a JSON Schema format description of the rule’s options. This schema will be used by ESLint to validate configured options and prevent invalid or unexpected inputs to your rule.
  • If your rule doesn’t expect any options, there is no action required. This change ensures that end users will not mistakenly configure options for rules that don’t expect options.
  • (not recommended) you can also set meta.schema to false to disable this validation, but it is highly recommended to provide a schema if the rule expects options and omit the schema (or set []) if the rule doesn’t expect options so that ESLint can ensure that your users’ configurations are valid.

The eslint-plugin/require-meta-schema rule can help enforce that rules have schemas when required.

Related Issues(s): #14709

FlatRuleTester is now RuleTester

As announced in our blog post, the temporary FlatRuleTester class has been renamed to RuleTester, while the RuleTester class from v8.x has been removed. Additionally, the FlatRuleTester export from eslint/use-at-your-own-risk has been removed.

To address: Update your rule tests to use the new RuleTester. To do so, here are some of the common changes you’ll need to make:

  • Be aware of new defaults for ecmaVersion and sourceType. By default, RuleTester uses the flat config default of ecmaVersion: "latest" and sourceType: "module". This may cause some tests to break if they were expecting the old default of ecmaVersion: 5 and sourceType: "script". If you’d like to use the old default, you’ll need to manually specify that in your RuleTester like this:

    // use eslintrc defaults
    const ruleTester = new RuleTester({
        languageOptions: {
            ecmaVersion: 5,
            sourceType: "script"
        }
    });
    
  • Change parserOptions to languageOptions. If you’re setting ecmaVersion or sourceType on your tests, move those from parserOptions to languageOptions, like this:

    ruleTester.run("my-rule", myRule, {
        valid: [
            {
                code: "foo",
                parserOptions: {
                    ecmaVersion: 6
                }
            }
        ]
    });
    
    // becomes
    
    ruleTester.run("my-rule", myRule, {
        valid: [
            {
                code: "foo",
                languageOptions: {
                    ecmaVersion: 6
                }
            }
        ]
    });
    
  • Translate other config keys. Keys such as env and parser that used to run on the eslintrc config system must be translated into the flat config system. Please refer to the Configuration Migration Guide for details on translating other keys you may be using.

Related Issues(s): #13481

Stricter RuleTester checks

In order to aid in the development of high-quality custom rules that are free from common bugs, ESLint v9.0.0 implements several changes to RuleTester:

  1. Test case output must be different from code. In ESLint v8.x, if output is the same as code, it asserts that there was no autofix. When looking at a test case, it’s not always immediately clear whether output differs from code, especially if the strings are longer or multiline, making it difficult for developers to determine whether or not the test case expects an autofix. In ESLint v9.0.0, to avoid this ambiguity, RuleTester now throws an error if the test output has the same value as the test code. Therefore, specifying output now necessarily means that the test case expects an autofix and asserts its result. If the test case doesn’t expect an autofix, omit the output property or set it to null. This asserts that there was no autofix.
  2. Test error objects must specify message or messageId. To improve the quality of test coverage, RuleTester now throws an error if neither message nor messageId is specified on test error objects.
  3. Test error object must specify suggestions if the actual error provides suggestions. In ESLint v8.x, if the suggestions property was omitted from test error objects, RuleTester wasn’t performing any checks related to suggestions, so it was easy to forget to assert if a test case produces suggestions. In ESLint v9.0.0, omitting the suggestions property asserts that the actual error does not provide suggestions, while you need to specify the suggestions property if the actual error does provide suggestions. We highly recommend that you test suggestions in detail by specifying an array of test suggestion objects, but you can also specify suggestions: <number> to assert just the number of suggestions.
  4. Test suggestion objects must specify output. To improve the quality of test coverage, RuleTester now throws an error if output property is not specified on test suggestion objects.
  5. Test suggestion objects must specify desc or messageId. To improve the quality of test coverage, RuleTester now throws an error if neither desc nor messageId property is specified on test suggestion objects. It’s also not allowed to specify both. If you want to assert the suggestion description text in addition to the messageId, then also add the data property.
  6. Suggestion messages must be unique. Because suggestions are typically displayed in an editor as a dropdown list, it’s important that no two suggestions for the same lint problem have the same message. Otherwise, it’s impossible to know what any given suggestion will do. This additional check runs automatically.
  7. Suggestions must change the code. Suggestions are expected to fix the reported problem by changing the code. RuleTester now throws an error if the suggestion test output is the same as the test code.
  8. Suggestions must generate valid syntax. In order for rule suggestions to be helpful, they need to be valid syntax. RuleTester now parses the output of suggestions using the same language options as the code value and throws an error if parsing fails.
  9. Test cases must be unique. Identical test cases can cause confusion and be hard to detect manually in a long test file. Duplicates are now automatically detected and can be safely removed.
  10. filename and only must be of the expected type. RuleTester now checks the type of filename and only properties of test objects. If specified, filename must be a string value. If specified, only must be a boolean value.
  11. Messages cannot have unsubstituted placeholders. The RuleTester now also checks if there are {{ placeholder }} still in the message as their values were not passed via data in the respective context.report() call.

To address: Run your rule tests using RuleTester and fix any errors that occur. The changes you’ll need to make to satisfy RuleTester are compatible with ESLint v8.x.

Related Issues(s): #15104, #15735, #16908, #18016

FlatESLint is now ESLint

As announced in our blog post, the temporary FlatESLint class has been renamed to ESLint, while the ESLint class from v8.x has been renamed to LegacyESLint.

To address: If you are currently using the ESLint class, verify that your tests pass using the new ESLint class. Not all of the old options are supported, so you may need to update the arguments passed to the constructor. See the Node.js API Reference for details.

If you still need the v8.x ESLint functionality, use the LegacyESLint class like this:

const { LegacyESLint } = require("eslint/use-at-your-own-risk");

Related Issues(s): #13481

Linter now expects flat config format

In ESLint v9.0.0, the config argument passed to Linter#verify() and Linter#verifyAndFix() methods should be in the flat config format.

Additionally, methods Linter#defineRule(), Linter#defineRules(), Linter#defineParser(), and Linter#getRules() are no longer available.

To address: If you are using the Linter class, verify that your tests pass.

If you’re passing configuration objects that are incompatible with the flat config format, you’ll need to update the code.

// eslintrc config format
linter.verify(code, {
    parserOptions: {
        ecmaVersion: 6
    }
});

// flat config format
linter.verify(code, {
    languageOptions: {
        ecmaVersion: 6
    }
});

Please refer to the Configuration Migration Guide for details on translating other keys you may be using.

Rules and parsers can be defined directly in the configuration.

// eslintrc mode
linter.defineRule("my-rule1", myRule1);
linter.defineRules({
    "my-rule2": myRule2,
    "my-rule3": myRule3
});
linter.defineParser("my-parser", myParser);
linter.verify(code, {
    rules: {
        "my-rule1": "error",
        "my-rule2": "error",
        "my-rule3": "error"
    },
    parser: "my-parser"
});

// flat config mode
linter.verify(code, {
    plugins: {
        "my-plugin-foo": {
            rules: {
                "my-rule1": myRule1
            }
        },
        "my-plugin-bar": {
            rules: {
                "my-rule2": myRule2,
                "my-rule3": myRule3
            }
        }
    },
    rules: {
        "my-plugin-foo/my-rule1": "error",
        "my-plugin-bar/my-rule2": "error",
        "my-plugin-bar/my-rule3": "error"
    },
    languageOptions: {
        parser: myParser
    }
});

If you still need the v8.x Linter functionality, pass configType: "eslintrc" to the constructor like this:

const linter = new Linter({ configType: "eslintrc" });

linter.verify(code, {
    parserOptions: {
        ecmaVersion: 6
    }
});

linter.getRules();

Related Issues(s): #13481

Change Language