# ESLint and Prettier

> Stop arguing about code style: Prettier formats automatically, ESLint catches real bugs and bad patterns, and together they end the bikeshedding.


---

# ESLint and Prettier

You've felt the pull-request comment that says "missing semicolon" and the other one that says "this `==` should be `===`." One of those is a waste of everyone's time and one of those is a real bug. This guide draws the line between the two: Prettier formats your code so nobody argues about whitespace again, and ESLint hunts the patterns that actually break things. Set them up once and your team stops bikeshedding for good.

## How to read this

Read it in order the first time. Phase 1 builds the mental model of two tools with two different jobs, because almost every painful ESLint+Prettier setup comes from confusing those jobs. Phase 2 is the day-to-day: configs, autofix, the commands you'll actually run. Phase 3 is enforcement and the gotchas that bite real teams. If you already have a working setup and only want it to stop fighting itself, skim Phase 1 then go straight to Phase 3.

## The phases

1. [Two tools, two jobs](01-two-tools-two-jobs.md) - the mental model: formatting versus linting, and why they're separate.
2. [Config and autofix](02-config-and-autofix.md) - how you really use them: config files, fixing on save, the everyday commands.
3. [Enforcement and gotchas](03-enforcement-and-gotchas.md) - editor, pre-commit, CI, and the conflicts that waste an afternoon.


---

# Two tools, two jobs

Here's the moment that makes this click. You open a pull request and get two comments. One says "indent this with 2 spaces, not 4." The other says "you wrote `if (user = admin)` - that's an assignment, not a comparison; it'll always be true." Both are about your code. They are not the same kind of problem at all. The first is taste dressed up as a rule. The second is a bug that will page someone at 3am.

Prettier handles the first kind. ESLint handles the second. Once you feel that split in your bones, every confusing thing about these tools straightens out.

## Formatting versus linting

**Formatting** is about how the code looks: indentation, line length, single versus double quotes, where the line breaks, whether there's a trailing comma. None of it changes what the code *does*. Two files with identical logic but different formatting run exactly the same.

**Linting** is about what the code *means*: unused variables, comparing with `==` when you meant `===`, using a variable before it's defined, an `await` that does nothing, a React hook called inside a condition. These are correctness and quality problems. They can change behavior, hide bugs, or signal a misunderstanding.

```text
FORMATTING (Prettier)          LINTING (ESLint)
-------------------            ----------------
indentation                   unused variables
quote style                   == vs ===
line length                   unreachable code
trailing commas               missing await
semicolons                    React hook misuse
spacing                       accidental globals
```

*What just happened:* the left column is cosmetic and has one defensible answer per project; the right column is about whether your code is correct. Different problems want different tools.

The reason this matters: a formatter can be *deterministic*. Feed Prettier the same file twice and you get byte-for-byte the same output, every time, on every machine. It doesn't have opinions you argue with - it has one opinion and applies it everywhere. A linter can't work that way, because "is this a bug?" is a judgment call with hundreds of separate rules, each one on or off.

## Why Prettier wins the style argument

Before Prettier existed, teams spent real hours in code review arguing about spacing and quote style. Style guides ran to dozens of pages. People configured ESLint with a hundred stylistic rules and then fought about which ones to enable.

Prettier ended that by being deliberately stubborn. It takes your code, throws away most of your formatting, and reprints it from scratch according to its own rules. You get a handful of knobs (print width, single versus double quotes, semicolons, tab width) and that's close to all of them. This sounds limiting. It's the whole point. When there's exactly one way the code can come out, there's nothing left to argue about.

```js
// what you typed (messy but valid)
const user = {name:"Ada",
    role:'admin',   skills:["math","logic"]}

// what Prettier prints (every time, everywhere)
const user = { name: "Ada", role: "admin", skills: ["math", "logic"] };
```

*What just happened:* Prettier didn't ask your preference. It normalized spacing, unified the quotes, added the trailing semicolon, and collapsed the object onto one line because it fit within the print width. Run it on a teammate's machine and the output is identical.

> The deepest value of a formatter isn't pretty code - it's that formatting stops being a *decision*. A decision nobody has to make is a meeting nobody has to have.

## Why ESLint stays in its lane

ESLint is the opposite kind of tool: a big, configurable engine that walks your code's structure and checks it against rules you choose. `no-unused-vars`, `no-undef`, `eqeqeq` (require `===`), `no-console` - each is a separate rule you can turn on, turn off, or set to warn instead of error. Plugins add rules for React, TypeScript, accessibility, imports, and more.

Because ESLint *can* check formatting too (it has old rules for indentation and quotes), people used to make it do both jobs. That's the classic mistake. When ESLint and Prettier both have an opinion about indentation, they fight: Prettier reformats a line, ESLint flags it, you fix it to satisfy ESLint, Prettier reformats it back. You spend an afternoon refereeing two tools that should never have overlapped.

```text
The conflict (what NOT to do):
  Prettier:  "this line should be indented 2 spaces" → reformats
  ESLint:    "indent rule says 4 spaces"            → errors
  You:       fix it → Prettier undoes it → loop forever
```

*What just happened:* two tools claiming the same job is the single most common ESLint+Prettier pain. The fix is structural, not clever: take formatting away from ESLint entirely and give it to Prettier. You'll do that in Phase 2 by turning off ESLint's stylistic rules.

The clean mental model: **Prettier owns how the code looks. ESLint owns whether the code is sound.** They don't overlap, so they don't fight. If you remember nothing else from this phase, remember that sentence.

## For builders

If your project uses JavaScript or TypeScript - a Node service, a React app, a CLI - you almost certainly want both. Prettier alone leaves real bugs in your code. ESLint alone leaves you arguing about commas. The combination is the default for a reason: it splits a messy human problem into two clean machine problems. And if you're still shaky on the language itself, the [JavaScript from zero](/guides/javascript-from-zero) guide is the foundation these tools sit on top of.

```quiz
[
  {
    "q": "Which problem is Prettier's job, not ESLint's?",
    "choices": ["Using == instead of ===", "An unused variable", "Inconsistent indentation", "A React hook called inside an if"],
    "answer": 2,
    "explain": "Indentation is purely cosmetic formatting - Prettier's territory. The other three are correctness/quality concerns that ESLint catches."
  },
  {
    "q": "Why can Prettier be deterministic but ESLint cannot?",
    "choices": ["Prettier is written in a faster language", "Prettier has one fixed way to print code; linting is hundreds of separate judgment-call rules", "ESLint runs on the server only", "Prettier doesn't read your code"],
    "answer": 1,
    "explain": "A formatter reprints code one fixed way, so output is identical everywhere. A linter is many independent on/off rules about whether code is correct."
  },
  {
    "q": "What causes the classic ESLint-vs-Prettier fight?",
    "choices": ["Running them in the wrong order", "Both tools having opinions about formatting at the same time", "Using TypeScript", "Forgetting to install Prettier"],
    "answer": 1,
    "explain": "When ESLint's stylistic rules and Prettier both format, they undo each other. The fix is to let Prettier own formatting and turn ESLint's style rules off."
  }
]
```


---

# Config and autofix

Now the everyday part. You know the two jobs; here's how you wire the tools into a project and run them. The good news: the modern setup is smaller than the old one. The pieces are a Prettier config (tiny), an ESLint config (a flat array these days), and two commands you'll run constantly - one to format, one to fix.

## Installing the pieces

Both tools live as dev dependencies in your project, not global installs. A global install means "works on my machine"; a project install means "works for everyone who clones this repo."

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

*What just happened:* `--save-dev` records both in `package.json` under `devDependencies`. Anyone who runs `npm install` afterward gets the exact same tools, so the team lints and formats identically.

For ESLint, the quickest, most straightforward start is its own setup command, which asks a few questions and writes a starter config for you:

```bash
npm init @eslint/config@latest
```

*What just happened:* ESLint scaffolds a config tailored to your answers (framework, TypeScript or not, browser or Node) and installs the plugins those answers imply. You'll still want to read what it wrote - a generated config you don't understand is a config you can't fix later.

## Prettier config: small on purpose

Prettier's config is deliberately tiny because Prettier doesn't have many knobs. A `.prettierrc.json` at your project root is enough:

```json
{
  "semi": true,
  "singleQuote": true,
  "printWidth": 100,
  "trailingComma": "all"
}
```

*What just happened:* you set four preferences - keep semicolons, prefer single quotes, wrap lines around 100 characters, add trailing commas everywhere they're legal. Everything else uses Prettier's defaults. Resist the urge to tweak more; the whole value is that the team stops debating these.

Pair it with a `.prettierignore` so Prettier skips files it shouldn't touch:

```text
dist
build
coverage
package-lock.json
```

*What just happened:* generated and vendored files are now off-limits to the formatter. Reformatting `package-lock.json` or your build output creates noisy diffs and helps nobody.

## ESLint flat config: the modern shape

Modern ESLint uses **flat config**: a single `eslint.config.js` file exporting an array of config objects. This replaced the older `.eslintrc` style. The array shape is the thing to understand - each object can target certain files and set rules, and later objects override earlier ones. That ordering is exactly what makes the Prettier handshake work.

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

export default [
  js.configs.recommended,        // ESLint's sensible default rules
  {
    rules: {
      eqeqeq: "error",           // require === over ==
      "no-unused-vars": "warn",  // flag unused vars, don't fail the build
    },
  },
];
```

*What just happened:* you started from ESLint's `recommended` ruleset, then layered your own object on top. Because your object comes later in the array, your `eqeqeq` and `no-unused-vars` settings win over anything earlier. `"error"` fails; `"warn"` shows a yellow warning but doesn't block.

## The handshake: turn off ESLint's style rules

This is the step that ends the fight from Phase 1. You install `eslint-config-prettier`, whose entire job is to switch *off* every ESLint rule that overlaps with formatting. After that, ESLint never has an opinion about a space or a quote, so it can never disagree with Prettier.

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

```js
// eslint.config.js
import js from "@eslint/js";
import prettier from "eslint-config-prettier";

export default [
  js.configs.recommended,
  {
    rules: {
      eqeqeq: "error",
      "no-unused-vars": "warn",
    },
  },
  prettier,   // MUST be last: disables ESLint's formatting rules
];
```

*What just happened:* `prettier` sits last in the array on purpose. Since later objects win, it turns off ESLint's stylistic rules *after* everything else has had its say. Now the division of labor is enforced in code: ESLint checks correctness, Prettier owns formatting, and they physically cannot collide. The order is the whole trick - put `prettier` anywhere but last and an earlier formatting rule can sneak back in.

> One sentence to memorize: `eslint-config-prettier` goes last and turns ESLint's formatting rules off. That's the entire peace treaty.

## The two commands you'll live in

Both tools have an autofix mode, and it's where most of the value is. You rarely fix style by hand - you let the machine do it.

```bash
# format every file in place
npx prettier --write .

# lint, and auto-fix the fixable problems
npx eslint . --fix
```

*What just happened:* `prettier --write .` rewrites every non-ignored file to match your config. `eslint . --fix` reports problems and automatically repairs the ones it safely can (like swapping `==` for `===`), leaving the judgment calls for you to fix by hand. The `.` means "this whole directory."

Wire them into `package.json` so nobody has to remember the flags:

```json
{
  "scripts": {
    "format": "prettier --write .",
    "lint": "eslint .",
    "lint:fix": "eslint . --fix"
  }
}
```

*What just happened:* now `npm run format` and `npm run lint` are the team's shared vocabulary. New contributors don't need to know `npx` incantations - they run the named scripts, which is also exactly what your CI will call in Phase 3.

## In the wild

A common rhythm on a healthy team: format-on-save in the editor (so you never think about it), `npm run lint` while you work (to catch bugs early), and both enforced in CI (so nothing slips through). You'll set up that enforcement next - the configs you wrote above are what every layer points at.

```quiz
[
  {
    "q": "What does eslint-config-prettier do, and where must it go?",
    "choices": ["Adds formatting rules to ESLint; goes first", "Turns off ESLint's formatting rules; goes last in the config array", "Replaces Prettier entirely; goes anywhere", "Installs Prettier; goes in package.json"],
    "answer": 1,
    "explain": "It disables ESLint's formatting rules so the two tools don't conflict. It must be last because later config objects override earlier ones."
  },
  {
    "q": "What is ESLint flat config?",
    "choices": ["A .eslintrc file in YAML", "A single eslint.config.js exporting an array of config objects", "A Prettier plugin", "A way to flatten nested folders"],
    "answer": 1,
    "explain": "Flat config is the modern format: one eslint.config.js that exports an array, where later objects override earlier ones."
  },
  {
    "q": "What does `eslint . --fix` do?",
    "choices": ["Reformats whitespace like Prettier", "Reports problems and auto-repairs the ones it safely can, leaving judgment calls for you", "Deletes files with errors", "Installs missing plugins"],
    "answer": 1,
    "explain": "--fix auto-corrects safely fixable issues (e.g. == to ===) and reports the rest. Whitespace formatting is Prettier's job, not ESLint's."
  }
]
```


---

# Enforcement and gotchas

A config that lives only on your laptop helps only you. The point of these tools is that the *whole team* writes consistent, bug-checked code - and that takes enforcement at three layers, each catching what the one before it missed. Then there are the few traps that waste an afternoon the first time you hit them. Let's cover both so they don't surprise you.

## Three layers, each a safety net for the last

Think of enforcement as nested nets. The editor catches things instantly. Pre-commit catches what you forgot to fix before committing. CI catches what slipped past a teammate whose editor wasn't set up. You want all three, because each layer assumes the one before it can be skipped.

```text
EDITOR        → fixes on save, instant feedback     (can be skipped)
PRE-COMMIT    → blocks the commit if it's not clean  (can be bypassed)
CI            → blocks the merge, no exceptions       (the real gate)
```

*What just happened:* the editor is convenience, pre-commit is a polite gate, CI is the gate that actually holds. The deeper you go, the harder it is to bypass - which is exactly the order you want, because the last layer is the one that protects the shared codebase.

### Layer 1: the editor

Install the Prettier and ESLint extensions for your editor and turn on format-on-save. In VS Code, that's a `.vscode/settings.json` committed to the repo so every contributor gets it:

```json
{
  "editor.formatOnSave": true,
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": "explicit"
  }
}
```

*What just happened:* every save now runs Prettier (formatting) and applies ESLint's safe fixes. Committing this file into `.vscode/` means a new teammate gets the behavior automatically instead of being told to "set up your editor" in an onboarding doc nobody reads.

### Layer 2: pre-commit hook

A pre-commit hook runs before a commit is recorded and can reject it. The standard combo is **husky** (manages the git hook) plus **lint-staged** (runs the tools on *only the files you're committing*, not the whole repo - fast).

```bash
npm install --save-dev husky lint-staged
npx husky init
```

```json
{
  "lint-staged": {
    "*.{js,ts,jsx,tsx}": ["eslint --fix", "prettier --write"],
    "*.{json,css,md}": ["prettier --write"]
  }
}
```

*What just happened:* `husky init` wires a git pre-commit hook. The `lint-staged` config says: for staged JS/TS files, run ESLint's fix then Prettier; for other files, format only. Because it touches only staged files, it stays fast even in a big repo. A commit with unfixable lint errors gets blocked before it exists.

### Layer 3: CI

Editors can be misconfigured and hooks can be bypassed with `--no-verify`. CI is the layer with no escape hatch. In CI you run the tools in *check* mode - they don't fix anything, they fail if the code isn't already clean.

```bash
# in CI: check only, never write
npx prettier --check .
npx eslint .
```

*What just happened:* `prettier --check` exits with an error if any file isn't already formatted (note `--check`, not `--write` - CI reports, it doesn't edit). `eslint .` fails on any error-level rule. A pull request that isn't clean can't merge. This is the layer that actually keeps the codebase consistent, because it's the one nobody can skip.

## Gotchas that waste an afternoon

**The conflict loop is back.** If Prettier and ESLint seem to undo each other's work, you almost certainly forgot `eslint-config-prettier`, or it isn't last in your flat-config array. Re-read Phase 2's handshake - order is everything.

**`--write` versus `--check`.** Run `prettier --write` in CI by accident and CI will silently "pass" by reformatting files in a throwaway container, fixing nothing in your repo. CI must use `--check`. Use `--write` only locally and in pre-commit.

**Linting the wrong files.** Flat config lints what it's pointed at, but you'll still want to ignore generated output. Add an ignores entry so ESLint doesn't waste time (and throw confusing errors) on `dist/` or `node_modules`:

```js
// eslint.config.js
export default [
  { ignores: ["dist", "build", "coverage"] },
  // ...rest of your config
];
```

*What just happened:* a config object with only an `ignores` key tells ESLint to skip those paths entirely. `node_modules` is ignored by default, but your own build output is not - list it explicitly or you'll get errors about code you didn't write.

**Don't fix the whole repo in one commit.** The first time you add Prettier to an old codebase, `prettier --write .` will touch hundreds of files. Do that as a *single isolated commit* with no logic changes, and record it so `git blame` can skip it:

```bash
# do the mass-format alone, then record the commit hash here:
echo "<commit-hash>" >> .git-blame-ignore-revs
```

*What just happened:* `.git-blame-ignore-revs` tells `git blame` to look past that giant formatting commit, so blame still points at whoever wrote the real logic instead of "the day we adopted Prettier." Keeping the reformat separate from feature work also keeps your diffs reviewable.

**Warnings that everyone ignores.** If a rule is set to `"warn"`, it shows up but never fails CI - and warnings nobody is forced to fix pile up until they're noise. Decide deliberately: a rule that matters should be `"error"`; a rule that's truly advisory can be `"warn"`; a rule you don't care about should be `"off"`, not a warning that trains the team to ignore the linter.

> The real test of your setup: clone the repo fresh, make a deliberately messy and slightly buggy change, and try to merge it. If the editor cleans the mess, the hook catches what's left, and CI blocks the bug - your three nets hold.

## In the wild

Mature teams treat a clean lint as non-negotiable as a passing test suite - same CI gate, same "fix it before merge" expectation. The payoff compounds: reviewers stop commenting on style entirely and spend their attention on logic and design, which is the only place human review was ever worth more than a machine.

```quiz
[
  {
    "q": "Why must CI use `prettier --check` instead of `prettier --write`?",
    "choices": ["--check is faster", "--write in CI reformats files in a throwaway container and falsely passes without catching anything", "--check also runs ESLint", "There is no difference in CI"],
    "answer": 1,
    "explain": "--write edits files (pointless in CI's disposable checkout), so it would always pass. --check fails when code isn't already formatted, which is what a gate needs."
  },
  {
    "q": "What is the role of lint-staged in a pre-commit hook?",
    "choices": ["It replaces ESLint", "It runs the tools on only the staged files, keeping the hook fast", "It pushes to CI", "It formats node_modules"],
    "answer": 1,
    "explain": "lint-staged runs ESLint/Prettier on just the files being committed, so the hook stays fast even in a large repo."
  },
  {
    "q": "Why record the mass-format commit in `.git-blame-ignore-revs`?",
    "choices": ["To delete the commit", "So git blame skips it and still attributes lines to whoever wrote the real logic", "To make CI ignore the files", "To re-run Prettier automatically"],
    "answer": 1,
    "explain": "A repo-wide reformat touches every line; without ignoring that commit, git blame would credit it instead of the original author."
  }
]
```
