You save a file, Prettier reformats it, and ESLint immediately flags an error on the line Prettier just touched. Both tools are configured, both are running, and they are fighting each other instead of helping you.

The two tools do different jobs. The trouble is that both have opinions about one thing: how code looks on the page.

What ESLint actually checks

ESLint parses your code into an abstract syntax tree and runs rules against its structure. Most of its value comes from rules that catch real bugs: a variable that’s never defined, an unreachable return after another return, an unused import left over from a refactor, a duplicate object key that silently overwrites the first. These are semantic checks. ESLint is reading what the code does.

ESLint also ships stylistic rules, like requiring single quotes or semicolons and capping line length — though as of ESLint 8.53 these are deprecated in favor of the separate ESLint Stylistic project, and they still don’t catch bugs. They catch disagreements about style, and that’s the part that collides with Prettier.

What Prettier actually does

Prettier doesn’t read your code for meaning at all. It parses it, throws away the original formatting, and reprints the whole file from its own rules for indentation, line length, quote style, and where to break long lines. Run it twice on the same input and you get byte-identical output — that’s the point. There is almost nothing to configure, by design: one .prettierrc, a handful of options, and every file in the project looks the same regardless of who wrote it.

Run prettier --check . in CI and it tells you whether every file matches that canonical formatting. It doesn’t know or care whether the code works.

Why ESLint and Prettier conflict

The overlap is narrow. ESLint’s stylistic rules (quote style, semicolons, indentation, trailing commas) cover the same ground as Prettier, and the two don’t always agree. When they disagree, you get the loop from the first paragraph: Prettier reformats a line, ESLint flags the result, you “fix” it by hand, Prettier puts it back.

So stop asking ESLint to make formatting decisions. Let it do only what Prettier can’t: catch bugs and bad patterns. eslint-config-prettier is a config that turns off every ESLint rule that could conflict with or duplicate Prettier’s output. It adds nothing. It only disables.

A second package, eslint-plugin-prettier, goes the other way: it runs Prettier as an ESLint rule, so every formatting difference shows up as its own ESLint error. Prettier’s own docs call this setup “generally not recommended”. It’s slower, because ESLint re-invokes Prettier per file. It’s also noisier: one reformat can turn into dozens of ESLint errors instead of one diff. Run Prettier standalone instead.

How to set up ESLint and Prettier with flat config

Since ESLint 9, the configuration file is eslint.config.js, a flat array of config objects that replaces the older .eslintrc format. If you’re starting a project from nothing:

1
npm init @eslint/config@latest

That walks you through a few questions and writes an eslint.config.js. Then add Prettier and the config that silences the conflicting rules:

1
npm install --save-dev --save-exact prettier eslint-config-prettier

eslint-config-prettier ships a flat-config entry point. Import it and put it last in the array. Later entries override earlier ones, so anything placed after it can turn conflicting rules back on:

1
2
3
4
5
6
7
8
// eslint.config.js
import js from "@eslint/js";
import eslintConfigPrettier from "eslint-config-prettier/flat";

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

Add a .prettierrc for Prettier’s own options (most teams need almost none of them):

1
2
3
4
{
  "semi": true,
  "singleQuote": true
}

And two scripts in package.json so both tools run independently:

1
2
3
4
5
6
{
  "scripts": {
    "lint": "eslint .",
    "format": "prettier --write ."
  }
}

eslint-config-prettier also ships a CLI check that lists any rule in your config that still conflicts with Prettier:

1
npx eslint-config-prettier eslint.config.js

Exit code 0 means no conflicts. 2 means it found some, and the output names each rule so you can remove it.

Using eslint-config-prettier with typescript-eslint

For TypeScript, typescript-eslint plugs into the same flat config array and adds its own semantic rules on top of plain ESLint — tseslint.configs.recommended bans any written directly in your code (no-explicit-any). Catching an any that leaked in from an untyped library call is a separate, heavier step: swap in recommendedTypeChecked, which needs parserOptions.project pointed at your tsconfig.json so the rules can ask the TypeScript compiler for real types. The ordering rule doesn’t change: eslint-config-prettier still goes last.

1
2
3
4
5
6
7
8
9
import js from "@eslint/js";
import tseslint from "typescript-eslint";
import eslintConfigPrettier from "eslint-config-prettier/flat";

export default [
  js.configs.recommended,
  ...tseslint.configs.recommended,
  eslintConfigPrettier,
];

Neither tool checks whether your types are well designed. A generic that should have been constrained with extends passes both; that’s the compiler’s job, covered in TypeScript Generics Explained With Examples.

Running ESLint and Prettier in your editor and CI

In the editor, set “format on save” to run Prettier, and let ESLint report separately, on save or in a pre-commit hook. Once the config files above exist, the editor extensions for both tools work independently. You don’t need to wire one through the other.

In CI, run both as separate steps:

1
2
npx eslint .
npx prettier --check .

A failed prettier --check means someone didn’t run the formatter. A failed eslint means the code has a real problem. Two steps keep that difference readable in the CI log.

Common ESLint and Prettier setup mistakes

The most common one is installing eslint-plugin-prettier because an old tutorial says to, then wondering why eslint . is slow and the problem list is three times longer than expected. Remove the plugin, keep eslint-config-prettier, run Prettier on its own.

The second is ordering. If a shared config or a project-specific override sits after eslintConfigPrettier in the array, it silently re-enables a rule that was turned off. The CLI check above catches this before it reaches a PR.

The third is scope. This pair is built for JavaScript and TypeScript. If you’re starting a new project and would rather have one faster tool doing both jobs, look at Biome. It’s a different tradeoff, not a drop-in replacement for the setup above.

What to do in your project today

Open your package.json. If eslint-plugin-prettier is there, uninstall it. Add eslint-config-prettier as the last entry in eslint.config.js, run npx eslint-config-prettier eslint.config.js, and fix whatever it names. After that, every ESLint error you see is about something ESLint found in the code’s structure, not a line Prettier would have reformatted anyway. It still won’t catch everything: forgetting await inside a loop or leaving a promise rejection unhandled is a logic mistake, not a lint error, unless you add a plugin for it — see JavaScript Async/Await and the Event Loop Explained for what those mistakes look like.