TypeScript

Prettier and ESLint Indent Rules Conflicting: the Fix

Stop fighting indent: Expected indentation of 2 spaces but found 4 by turning off the rule that conflicts with Prettier, not by retuning it.

Mahdi Ben RhoumaMahdi Ben Rhouma
8 min read

A .eslintrc.js sets indent: ['error', 2]. Prettier reformats the file with tabWidth: 2. ESLint still fails:

text
Expected indentation of 2 spaces but found 4. (indent)

Both tools are configured for 2 spaces. Neither is wrong by its own rules, and the error does not go away by nudging the number.

TL;DR#

ESLint's indent rule and Prettier's formatter are two independent implementations of "how should this be indented", built by different projects with different edge-case decisions (switch/case, JSX attributes, chained calls, ternaries). They will diverge on some real files even when both are set to the same number of spaces. The fix is not to retune indent — it is to turn it off and let Prettier own indentation entirely, with eslint-config-prettier doing that in one line instead of a manual rule-by-rule disable.

Why it happens#

This exact conflict was raised against ESLint itself. In eslint/eslint#10930, ESLint maintainer Kai Cataldo confirmed the mechanism in a comment the original Stack Overflow answer quotes: ESLint's indent rule and Prettier are "completely separate implementations" of the same formatting goal — two different approaches to the same problem — and, in Cataldo's words, "it's guaranteed that they will clash."

There is a second, more current reason the conflict is getting less visible by the day: ESLint's core indent rule is now deprecated. ESLint's own rule metadata lists it among the rules replaced by the separate @stylistic/eslint-plugin package, which now owns all of ESLint's old formatting rules (indent, quotes, semi, and the rest). The core rule still ships and still runs if a config turns it on — ESLint has not removed it — but no current recommended config enables it by default, and a project that still has indent: ['error', 2] sitting in an old .eslintrc.js inherited it from a 2019-era config, not from anything ESLint ships today.

So the realistic 2026 version of this bug is one of two configs:

  1. An old .eslintrc.js (or a flat eslint.config.js that still imports @stylistic/eslint-plugin for indent) enabling a formatting rule that duplicates Prettier.
  2. A shared config (airbnb, standard, a team preset) that enables indent or a @stylistic/@typescript-eslint indentation rule internally — handled by placing "prettier" after it in "extends", step 2 below.

Fix#

1. Install eslint-config-prettier#

bash
npm install --save-dev eslint-config-prettier

This package does not format anything. It only turns off the ESLint rules that fight Prettier — including the core indent, the @stylistic/eslint-plugin equivalents, and the @typescript-eslint indentation rules, per its own plugin list. A current eslint-config-prettier covers all of those plugins from the single "prettier" entry — its own README notes that older guides recommending a separate "prettier/react" or "prettier/@typescript-eslint" config are no longer necessary. If ESLint instead stops on a TypeScript file with Parsing error: The keyword 'enum' is reserved, Prettier is not the cause: set @typescript-eslint/parser as the parser.

2. Put it last, in both config formats#

With the legacy .eslintrc.* format (still supported, no longer the default since ESLint 9):

json
{
  "extends": [
    "plugin:@typescript-eslint/recommended",
    "prettier"
  ]
}

"prettier" must be the last entry so it overrides the rules the earlier configs turned on — ESLint applies "extends" entries in order, and only a later one can disable a rule an earlier one enabled.

With a flat eslint.config.js (the ESLint 9+ default, confirmed by the current configuration files doc):

js
import tseslint from 'typescript-eslint';
import eslintConfigPrettier from 'eslint-config-prettier/flat';
 
export default [
  ...tseslint.configs.recommended,
  eslintConfigPrettier,
];

Flat config has no implicit override order from "extends" — array position is the order, so eslintConfigPrettier must be the last array entry for the same reason.

3. Delete the manual indent rule entirely#

Do not leave indent: ['error', 2] in the rules block and expect "prettier" to beat it. eslint-config-prettier's own docs are explicit about this: for the legacy format, a rule set directly in "rules" always overrides one disabled by an extended config, because that is how "extends" is designed to work. The config package cannot touch your own rules block — only the CLI helper tool below can flag it.

diff
  rules: {
-   indent: ['error', 2],
    quotes: ['error', 'single'],
  }

4. Catch what you missed with the CLI helper#

bash
npx eslint-config-prettier path/to/a/real/file.js

This reports any rule in your own rules block (not an extended config) that conflicts with Prettier — exactly the case step 3 fixes. It is shipped inside eslint-config-prettier itself, so no extra install is needed.

5. Only add eslint-plugin-prettier if you want Prettier diffs as lint errors#

eslint-config-prettier disables conflicting rules; it does not run Prettier. If the goal is also to fail lint on formatting drift (instead of running prettier --check separately), add eslint-plugin-prettier and its bundled recommended config, which wires up both packages in one step:

js
// eslint.config.js
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
 
export default [
  // other configs
  eslintPluginPrettierRecommended,
];

or, legacy format: "extends": ["plugin:prettier/recommended"], last in the array.

Verify the fix#

  1. Run npx eslint . on a file Prettier just reformatted. The indent error (or its @stylistic/@typescript-eslint equivalent) must be gone — if it still fires, run npx eslint-config-prettier <file> to find the rule enabled directly in rules (step 3).
  2. Run npx eslint --print-config path/to/file.js | grep '"indent"' (or the equivalent on Windows) and confirm the rule's severity is "off" (0), not just absent — an unset rule can still be turned on by a shared config loaded afterwards.
  3. Format a file with a construct the two tools are known to disagree on — a switch/case block or a chained method call spanning several lines — and confirm ESLint reports nothing on it after a Prettier pass.

One email a month — the latest explained errors

What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.

What a shared config changes#

A shared config such as airbnb-base or standard can still enable indent or a @typescript-eslint indentation rule inside itself. Because it is extended before "prettier" (or placed before it in a flat config array), "prettier" still disables it — the override order from step 2 is exactly what handles this case, with no extra step. The one thing that order cannot fix is a rule set directly in the project's own rules block (step 3), because "extends" is designed to let a project's own rules win.

If ESLint next reports a rule definition that cannot be found — the same "not found" family as Definition for rule 'import/extensions' was not found, which happens when the plugin that owns a rule is not registered in the config — see the import/extensions fix for how to register a missing plugin.

Next.js projects hit the flat-config version of this fix directly: Next.js 16 removed next lint and the eslint next.config option entirely, so linting runs as a plain eslint.config.mjs with no Next.js wrapper. See the ESLint 9 flat-config migration guide for the codemod and the eslint-config-next flat-config subpaths. And if a rule-config comment such as // eslint @typescript-eslint/no-explicit-any: "off" is silently ignored instead of a conflicting-formatting error, that is a different ESLint footgun with its own fix in Unexpected any. Specify a different type: fix or disable.

Indentation conflicts are the most visible symptom of running two formatting engines on the same files, but they are not the only one — quotes, semicolons and trailing commas can clash the same way. eslint-config-prettier disables all of them from the same plugin list, which is why disabling the conflicting rules, rather than tuning them to match, is the fix that holds regardless of which construct triggers the next disagreement.

Further reading: Fix 'originalKeywordKind' DeprecationError in TypeScript. Also see Fix ESLint: Module.createRequire is not a function. Also see Fix Cannot resolve symbol 'Routes': stale JetBrains typings. Also see Fix 'ESLint was configured to run on' in Cypress tests.

Frequently Asked Questions

|

Have more questions? Contact us

Still stuck? Paste your error.

Paste the error message or stack trace — we check it against every fix and tool on the site.

Matching happens in your browser. When you search, a cleaned error signature is sent so we can spot errors we do not cover yet — never the raw text, your IP address or your e-mail. Do not paste passwords or keys. Details

Written by

Mahdi Ben Rhouma
Mahdi Ben Rhouma

Full-Stack Dev — Next.js & Supabase

Solo developer building SaaS products with Next.js and Supabase. Writing about production patterns the official docs skip.

Remote