Clean Code with ESLint and Prettier

ESLint reads your code and flags likely mistakes, like an unused variable or a == that should be ===, and Prettier formats every file the same way, so you set them up once with a flat config and npm scripts and let VS Code run them as you save.

Journeyman · JavaScript · Node.js · Software Development · Updated

After this lesson you can

  • Set up ESLint with a flat config in eslint.config.js and fix the problems it reports
  • Format code with Prettier from an npm script and on save in VS Code, and tell what Prettier changes from what ESLint reports

Key takeaways

  • A linter such as ESLint finds likely mistakes without running the code. A formatter such as Prettier only changes how code looks, never what it does.
  • ESLint's settings live in eslint.config.js, a flat config. It lists rule sets such as js.configs.recommended and single rules such as eqeqeq.
  • Each problem ESLint reports gives the line and column, a message and the rule's name. Fix the code itself; don't switch the rule off.
  • prettier --check . lists the files that need formatting, and prettier --write . formats them. Teams run both tools from npm scripts.
  • With the Prettier extension and Format On Save turned on, VS Code formats each file every time you save it.

On a team, your code is read before it's used. Someone reviews every change, and the first things a reviewer spots are the small ones: a variable nobody uses, a == that should be ===, quotes of two kinds in one file. On many teams, code can't even be merged until two tools say it's clean. Those two tools are ESLint and Prettier. Set them up once, and they catch those small things for you, before anyone else sees them, in every project from a class assignment to a company's website.

A spell-checker and an auto-format button

A word processor helps you in two different ways. Its spell-checker underlines words that look wrong: it doesn't change anything, it points, and you decide. Its formatting tools set the fonts, the spacing and the margins the same way everywhere, so you stop thinking about them.

Code has the same two helpers:

  • A linter is the spell-checker. It reads your code without running it and points out likely mistakes: a variable you made and never used, a name that's misspelled, a comparison that can surprise you. ESLint (said "E-S-lint") is the linter for JavaScript.
  • A code formatter is the formatting button. It rewrites how the code is laid out (quotes, semicolons, spaces, indents, line breaks) without changing what it does. Prettier is the formatter most JavaScript projects use.

They don't overlap. ESLint doesn't care how many spaces you use, and Prettier never changes what your code does.

Install the tools

In the game-night-scoreboard project from A Dev Server and Build with Vite, install four packages as devDependencies. --save-exact writes each version without a ^, so everyone on the project gets exactly the same rules:

npm install --save-dev --save-exact eslint@10.13.0 @eslint/js@10.0.1 globals@17.13.0 prettier@3.9.10
added 80 packages, and audited 97 packages in 3s

28 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
  • eslint is ESLint itself.
  • @eslint/js holds ESLint's recommended rules for JavaScript. The @eslint/ part, read "at eslint slash", says the package belongs to the ESLint team.
  • globals lists the names a browser or Node already provides, such as document and console, so ESLint knows they're real.
  • prettier is Prettier.

Four packages went in, and 80 arrived: the tools need packages of their own, and npm installs those too.

ESLint's settings: eslint.config.js

ESLint reads its settings from a file named eslint.config.js at the top of the project. Make it with this:

import js from "@eslint/js";
import globals from "globals";
import { defineConfig, globalIgnores } from "eslint/config";

export default defineConfig([
  globalIgnores(["dist"]),
  js.configs.recommended,
  {
    languageOptions: {
      globals: globals.browser,
    },
    rules: {
      eqeqeq: "error",
      "prefer-const": "error",
    },
  },
]);

This is called a flat config: one flat list, in square brackets, of settings that apply in order. Each entry does one thing:

  • globalIgnores(["dist"]) tells ESLint to skip the dist folder. It's the bundler's minified output, not code you wrote, and without this line ESLint reports dozens of problems in it.
  • js.configs.recommended turns on the rules the ESLint team recommends for everyone, such as no-unused-vars (a variable that's never used) and no-undef (a name that was never declared).
  • globals.browser says this code runs on a web page, so document and window are real names. Code that runs in Node uses globals.node instead.
  • rules turns on two more rules. eqeqeq asks for === (read "triple equals", or "strictly equal") instead of ==. prefer-const asks for const when a let is never given a new value. Each rule is set to "error", "warn" or "off".

Read what ESLint reports

Add a winner function to the end of src/scores.js. It works, but it has three things a reviewer would flag:

export function winner(players, rounds) {
  let table = standings(players, rounds);
  const roundsPlayed = rounds.length;
  if (table[0].total == table[1].total) {
    return "It's a tie!";
  }
  return table[0].player;
}

npx (read "en-pee-ex") runs a command from a package installed in the project. Run ESLint on every file in the project; the . (read "dot") means "this folder and everything in it":

npx eslint .
C:\Users\...\game-night-scoreboard\src\scores.js
  20:7   error  'table' is never reassigned. Use 'const' instead   prefer-const
  21:9   error  'roundsPlayed' is assigned a value but never used  no-unused-vars
  22:22  error  Expected '===' and instead saw '=='                eqeqeq

✖ 3 problems (3 errors, 0 warnings)
  1 error and 0 warnings potentially fixable with the `--fix` option.

Each problem has four parts: the line and column (20:7 is line 20, character 7), how serious it is, what's wrong, and the name of the rule. Fix each one in the code:

Rule What it caught The fix
prefer-const table never changes after it's made Change let table to const table
no-unused-vars roundsPlayed is never used Delete the line
eqeqeq == converts types before comparing: 0 == "" is true Change == to ===

Why does == matter? As in Booleans and Comparisons, == changes the types until they match, so "7" == 7 is true, and so is 0 == "". With scores typed into a form, a player's 0 and an empty box would count as a tie. Run npx eslint . again, and it prints nothing at all: no news is good news. npx eslint . --fix fixes some rules, such as prefer-const, by itself. Read what it changed.

Prettier: format, don't argue

Here's the same winner function, typed in a hurry: crooked indents, no spaces, single quotes, no semicolons.

export function winner(players,rounds){
    const table=standings(players,rounds)
  if(table[0].total===table[1].total){ return 'It\'s a tie!' }
    return table[0].player
}

ESLint finds nothing wrong with it: none of its rules is about layout. That's Prettier's job. --check lists the files Prettier would change, and changes nothing:

npx prettier --check .
Checking formatting...
[warn] src/scores.js
[warn] Code style issues found in the above file. Run Prettier with --write to fix.

npx prettier --write . formats them. It names each file it read, with (unchanged) after those that were already fine, and leaves out whatever .gitignore lists, such as node_modules and dist:

eslint.config.js 17ms (unchanged)
index.html 13ms (unchanged)
package-lock.json 15ms (unchanged)
package.json 1ms (unchanged)
src/main.js 6ms (unchanged)
src/scores.js 5ms
src/style.css 11ms (unchanged)

src/scores.js is formatted:

export function winner(players, rounds) {
  const table = standings(players, rounds);
  if (table[0].total === table[1].total) {
    return "It's a tie!";
  }
  return table[0].player;
}

Teams that use Prettier stop arguing about tabs, spaces and quotes: the tool decides, the same way for everyone. (A .prettierrc file can change a few of its choices, such as { "singleQuote": true }, but most projects keep the defaults.)

npm scripts and format on save

Teams don't type these commands from memory: they're npm scripts. Add three to package.json, next to Vite's:

  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview",
    "lint": "eslint .",
    "format": "prettier --write .",
    "format:check": "prettier --check ."
  },

Now npm run lint, npm run format and npm run format:check work the same on every computer. format:check changes nothing, so it's the one an automatic check runs before code is merged.

Better still, let VS Code run the tools while you type:

  1. Install the two extensions. In VS Code's terminal, run code --install-extension dbaeumer.vscode-eslint and code --install-extension esbenp.prettier-vscode. ESLint's problems now show as squiggly underlines as you type, and hovering over one shows the rule.
  2. Turn on Format On Save. Open File > Preferences > Settings (Code > Settings on a Mac), search for format on save and tick Editor: Format On Save. Then search for default formatter and choose Prettier - Code formatter.

Now every save formats the file. Type messy, press Ctrl+S, and it's tidy.

Try it. Turn on Format On Save, open src/main.js, and mess up its indents and quotes. Save, and watch Prettier put them back. Then add let unused = 5; to the file and look for the squiggle.

Check yourself

2 questions to try on the lesson page.

So what

ESLint catches the mistakes that slip past you, and Prettier makes every file look like the same careful person wrote it. With both running on save, you hand in code that's ready for review, every time.

Key terms

Cheat sheets

Related