Topic 12.5
Linting and Formatting
In one line
A linter such as ESLint reads your code without running it and reports likely bugs and banned patterns; a formatter such as Prettier rewrites the layout (spaces, quotes, line breaks) into one consistent style; your editor and a pre-commit check run both automatically.
Think of it like this
A teacher and a typist. Before you hand in an essay, a teacher reads it and circles real problems: "this sentence contradicts the last one", "you used a word that doesn't exist". That's the linter. A typist then retypes the whole essay neatly with the same margins, font and spacing as everyone else's, without changing a single word's meaning. That's the formatter. Teacher for mistakes, typist for looks.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Static analysis
- Checking code by reading it, without running it.
- Linter
- A tool that reads code and reports likely bugs and banned patterns, using a list of rules.
- Rule
- One check a linter applies, such as "no unused variables", with a severity of off, warn or error.
- Formatter
- A tool that rewrites the layout of code (spaces, quotes, line breaks) into one consistent style.
- Auto-fix
- When the tool rewrites the code itself to solve a problem it found.
- Flat config
- ESLint's current settings format: one
eslint.config.jsfile exporting an array of settings objects. - Globals
- Names that exist without being declared or imported, like
documentin a browser orprocessin Node.js. - Git hook
- A script Git runs automatically at a moment such as just before a commit.
Step by step
01Install ESLint and Prettier
Both are dev tools, so they go in devDependencies (Topic 12.3). globals is a small package listing browser and Node global names. npm init @eslint/config@latest can also ask you questions and write the config for you.
02Write the ESLint config
Start from the recommended rules, declare browser globals, add a couple of team rules, and put eslint-config-prettier last so it can switch off anything that overlaps with formatting.
import js from "@eslint/js";
import globals from "globals";
import prettier from "eslint-config-prettier";
export default [
{ ignores: ["dist/"] },
js.configs.recommended,
{
languageOptions: { globals: globals.browser },
rules: {
eqeqeq: "error",
"no-var": "error",
"no-console": "warn",
},
},
prettier,
];03Lint some code
Here is a file with four problems. Run ESLint on the project.
export function total(items) {
var sum = 0;
for (const item of items) {
if (item.qty == "0") continue;
sum += item.price * item.qty;
debugger;
}
const discount = 0.1;
return sum;
}04Read the report
Each line gives line:column, the severity, the message and the rule name. Look a rule up on eslint.org to learn why it exists. One problem is auto-fixable.
05Format with Prettier
--write rewrites files in place; --check only reports, which is what CI uses. A .prettierrc file holds the few options you change, and .prettierignore lists folders to skip (it already skips node_modules).
{
"printWidth": 100,
"singleQuote": false,
"trailingComma": "all"
}06Automate it: editor, commit, CI
Add scripts so everyone runs the same commands, editor settings so it happens on save, and a CI step so nothing slips through.
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
}
}Try it yourself
- 1
Find the fourth rule
In the second example, add a rule
no-consolethat reports any line containingconsole.. Add a lineconsole.log(sum);tosourceand predict the new report line, column included. - 2
Fool the tiny linter
Add the line
const note = "never use var here";tosource. Predict what the tiny linter reports. Why would real ESLint stay quiet? - 3
Change the print width
In the third example, find the smallest
printWidththat keeps the object on one line. Count first: how long is the one-line version?
Code & diagrams
No error appears, and three of these are real bugs. That's the point of a linter: it finds problems that don't crash, by reading the code. ESLint's recommended set catches the last three; `eqeqeq` is one line to add.
Preview
Console
Nothing logged yet.Expected console output
0 == '' is true but 0 === '' is false
theme: light
Everyone is admin now! role = admin
total: 5Positions are `line:column`, both counted from 1, like ESLint's report. Real ESLint doesn't search text: it parses the code into a tree first, so `"var "` inside a string or a comment never triggers `no-var`.
Preview
Console
Nothing logged yet.Expected console output
2:3 error Unexpected var, use let or const instead no-var
4:18 error Expected '===' and instead saw '==' eqeqeq
6:5 error Unexpected 'debugger' statement no-debugger
3 problemsMixed quotes and random spaces disappear because the formatter prints from the structure, not the text. If the line fits within `printWidth` it stays on one line; otherwise each property gets its own line with a trailing comma, like Prettier's default output.
Preview
Console
Nothing logged yet.Expected console output
const user = { name: "Asha", city: "Pune", plan: "pro", seats: 3 };
const user = {
name: "Asha",
city: "Pune",
plan: "pro",
seats: 3,
};Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Forget to declare browser globals
In eslint.config.js, remove the languageOptions: { globals: globals.browser } line and run npx eslint . on code that uses document.
Break #2
Commit without formatting
Turn off format on save, write code with mixed quotes and odd spacing, commit, and let CI run npx prettier . --check.
Myth vs fact
Myth
Linters and formatters do the same job.
Fact
A linter finds code-quality problems and possible bugs; a formatter only changes layout. Use both, and let each own its area (eslint-config-prettier keeps them from fighting).
Myth
If the code runs, the linter's complaints don't matter.
Fact
Many lint errors are bugs that don't crash: duplicate keys, assignments in conditions, unused variables you meant to use. They show up later as wrong results.
Myth
Prettier's style is just someone's opinion forced on everyone.
Fact
It is an opinion, and that's the benefit: one automatic style means no time spent arguing or reviewing spacing, and diffs only show real changes.
Myth
Disabling a rule with a comment is cheating.
Fact
Sometimes a rule is wrong for one line. Disable it for that line only, name the rule, and add the reason; turning rules off for whole files hides real problems.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
ESLint parses code with Espree into an ESTree-shaped AST; each rule registers visitors such as
BinaryExpression(node)and reports with a location. Writing a custom rule for your codebase ("don't callfetchdirectly, useapi()") is a short file. Type-aware linting (typescript-eslintwith type information) catches things like a forgottenawaiton a promise (no-floating-promises), at the cost of slower runs. - ▸
Prettier's core algorithm comes from Philip Wadler's "A prettier printer" paper: it builds a document of groups and tries to fit each group on one line within
printWidth, breaking the outermost group first when it doesn't fit. - ▸
Speed matters at scale: lint only changed files in pre-commit hooks (lint-staged), cache results (
eslint --cache), and consider Rust-based tools such as Oxlint or Biome, which are many times faster and implement most popular ESLint rules. - ▸
Rules are documentation of team decisions. Turning on
no-restricted-importsorno-restricted-syntaxcan encode architecture rules ("UI code may not import fromserver/") so they are checked on every commit instead of remembered.
Remember this
- 1
Static analysis means checking code by reading it, without running it. A linter is a static-analysis tool that applies a set of rules. ESLint is the standard linter for JavaScript. Its rules catch real bugs:
no-undef(a misspelled variable such asdocment),no-unused-vars(a variable you meant to use but didn't),no-dupe-keys(the same key twice in an object, where the last one silently wins),no-cond-assign(if (x = 5)instead of===),no-unreachable(code afterreturn). Others enforce team choices:eqeqeq(always===),no-var(uselet/const). - 2
Each rule has a severity:
"off","warn"(reported, but doesn't fail the run) or"error"(the command exits with a failure code, which stops a CI build). Many rules are auto-fixable:eslint --fixrewrites the code for you (for examplevartolet). Rules can be turned off for one line with a comment,// eslint-disable-next-line no-console, which you should use rarely and explain. - 3
ESLint 9 uses flat config: one
eslint.config.jsfile that exports an array of config objects. You usually start withjs.configs.recommended(a curated rule set), tell ESLint which globals exist (browser ones such aswindowanddocument, or Node ones such asprocess) throughlanguageOptions.globals, and add your own rules. Plugins add rules for frameworks and languages:typescript-eslintfor TypeScript,eslint-plugin-react-hooksfor React. The older.eslintrcformat is no longer supported in ESLint 10. - 4
A formatter only changes layout: indentation, line length, quote style, semicolons, trailing commas, spacing. Prettier is the most used one. It doesn't edit your text: it parses the code into a tree (the AST from Topic 0.1) and prints it again from scratch in its own style, so the way you typed it no longer matters. Prettier has very few options on purpose (
printWidth, default 80;singleQuote;semi;tabWidth;trailingComma), which ends style debates in code review. Prettier also formats HTML, CSS, JSON and Markdown. Biome is a newer, faster tool that does both linting and formatting. - 5
Keep the two jobs separate. Since ESLint 8.53, ESLint's own formatting rules are deprecated, and if you use a shared config that still has them, add **
eslint-config-prettier, which turns off every ESLint rule that could fight Prettier. Then ESLint reports problems and Prettier owns the look. Run them in three places: in the editor (format on save, lint errors underlined as you type), before each commit (a Git hook with tools such as husky and lint-staged checks only the changed files), and in CI** (eslint .andprettier --check .fail the build if anyone skipped the first two). - 6
Make the editor setup part of the project, so everyone gets it automatically. In VS Code, install the ESLint and Prettier extensions and commit a
.vscode/settings.jsonwith"editor.formatOnSave": trueand"editor.defaultFormatter": "esbenp.prettier-vscode". An **.editorconfig** file sets basics (indent size, line endings, final newline) in almost every editor. The aim: nobody thinks about formatting, and lint problems are fixed while the code is still fresh in your head.
Explain it without notes
What is the difference between a linter and a formatter? Name the usual tool for each.
Give three bugs ESLint catches that don't cause an error when the code runs.
How do you stop ESLint and Prettier from disagreeing?
Where should linting and formatting run in a team, and why in more than one place?
Practice
Write an eslint.config.js for a browser project that uses the recommended rules, makes eqeqeq an error and no-console a warning, and ignores the dist folder.
Write a function findLooseEquals(lines) that returns the 1-based line numbers containing == or != but not === or !==. Test it on ["a === b", "a == b", "a != b"].
Trade-offs
- ↔
Strict rule sets catch more problems but can slow people down with noise; start from
recommended, add rules that prevent bugs you've actually had, and prefererrorover many ignoredwarns. - ↔
Pre-commit hooks give fast feedback but can be skipped (
--no-verify) and slow down commits if they lint the whole project; CI checks can't be skipped but report later.
Done when you can
I can explain the difference between a linter and a formatter.
I can install ESLint and Prettier and write a flat
eslint.config.jswith globals and rules.I can read an ESLint report: position, severity, message, rule name.
I can set up format on save and keep ESLint and Prettier from conflicting.
I know why teams run checks in the editor, before commits and in CI.