ESLint Flat Config: A Practical Migration Decision Guide
Flat config replaces .eslintrc with an array of JavaScript objects. Learn the merge semantics, plugin registration, and migration pitfalls that cause silent lint failures.
13 Feb 2026, 23:42 UTC

The hidden risk in renaming .eslintrc to eslint.config.js
Switching to ESLint's flat configuration is not a file rename. The moment eslint.config.js exists, ESLint stops reading .eslintrc* files entirely. If you copy a subset of rules into the new format and leave the old files in place, you have not migrated—you have silently disabled linting for every path the new config does not explicitly match. The practical decision is whether your team can validate the new resolution model before deleting the legacy setup.
What the configuration array actually does
Flat configuration replaces the old cascading .eslintrc hierarchy with a single JavaScript module that exports an array of plain objects. Each object can carry a files glob, an ignores glob, parser settings under languageOptions, a plugins map, and a rules map. There is no overrides array and no extends string resolution; instead, ESLint evaluates the array from top to bottom and applies matching objects to the file being linted.
A minimal flat config
The following example enables two rules for JavaScript files and registers a plugin under a namespace. It assumes ESLint 9 or ESLint 8 with flat config explicitly enabled.
// eslint.config.js
import js from "@eslint/js";
import importPlugin from "eslint-plugin-import";
export default [
{
files: ["**/*.js"],
languageOptions: {
ecmaVersion: "latest",
sourceType: "module",
},
plugins: {
import: importPlugin,
},
rules: {
"no-unused-vars": "warn",
"no-console": "error",
},
},
];
Notice that the plugin is imported as a JavaScript object and assigned to a key. That key becomes the prefix for the plugin's rules, such as import/no-unresolved. The legacy string form "import" is not valid here.
Merge order is the API
Configuration objects are evaluated sequentially and merged shallowly. A later object that matches the same file overrides properties from earlier objects, but it does not deep-merge nested objects like languageOptions. The practical consequence is ordering: place shared presets first and team- or directory-specific overrides last.
export default [
js.configs.recommended,
{
files: ["**/*.js"],
rules: {
"no-console": "error",
},
},
{
files: ["scripts/**/*.js"],
rules: {
"no-console": "off",
},
},
];
In this arrangement, files under scripts/ receive the later no-console setting and suppress the error, while other JavaScript files keep it. Because the merge is shallow, if a later object redefines languageOptions, it replaces the entire object rather than merging individual keys.
Global ignores require their own object
Patterns that should exclude files from linting entirely must appear in a configuration object that contains only the ignores key. Mixing ignores with files, rules, or any other property in the same object is invalid and will cause ESLint to reject the configuration.
export default [
{
ignores: ["dist/", "coverage/", "*.min.js"],
},
// ... remaining config objects
];
Version boundaries and migration traps
Flat configuration is the default starting in ESLint 9. In ESLint 8, it is available but must be enabled with the environment variable ESLINT_USE_FLAT_CONFIG=true, and some behaviors may differ subtly. Before changing files, confirm the installed major version:
# Run in the project root
npx eslint --version
If the output shows 9.x or later, flat config is already the default. If it shows 8.x, decide whether to set the environment variable in CI and local scripts or upgrade first.
Two traps cause most silent failures. First, as noted above, legacy .eslintrc* files are ignored completely once flat config is active, so an incomplete migration removes rules without an error message. Second, glob patterns in files are resolved relative to the directory containing eslint.config.js, not necessarily the project root. In a monorepo, a pattern like **/*.js in a package-level config will match files in that package's directory tree, which can produce unexpected matches if the config file sits above or below the intended scope.
Verifying the merged result
Because the array model makes resolution less obvious than the old cascade, verify the exact configuration applied to a real file before committing:
# Run in the project root; replace the path with a file you lint regularly
npx eslint --print-config src/index.js
The command prints the fully merged configuration object for that path. Check that the rules you expect are present and that plugin namespaces appear as registered. This does not require write permissions, but it does require that dependencies are installed.
To confirm a rule actually fires, create a temporary file with a known violation and lint it directly:
# Create tempfile.js containing: console.log("test");
npx eslint tempfile.js
If no-console is set to error, ESLint should exit with a non-zero status and report the violation. If it exits cleanly, either the file is not matched by any files pattern or a later config object overrode the rule.
Rolling back a partial migration
Adopting flat config changes project state. If validation fails, the safest rollback is to remove eslint.config.js, restore the original .eslintrc* files from version control, and unset ESLINT_USE_FLAT_CONFIG=true in any scripts or CI environment where you added it. Keeping the legacy files untouched until the new configuration passes the checks above makes this rollback a single commit revert rather than a reconstruction effort.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.