Open any JavaScript project on GitHub. There's an eslint.config.js (or .eslintrc), a .prettierrc, a npm run lint script in package.json, and a CI step that runs both. New contributors clone the repo, write their first change, push, and CI fails on something they've never heard of. The error says no-unused-vars or prefer-const or "code style issues found". Nobody ever sat them down and explained what these tools do or why every project has them.
This chapter does that. By the end you'll know exactly what ESLint catches, what Prettier formats, why they're two tools instead of one, how to wire them into VS Code so they run as you type, and how to add the same lint step to CI from Ch 15.
The Two Problems Each Tool Solves
When more than one person edits a codebase, two annoying classes of issue keep showing up in code review:
-
Real bugs that the runtime won't catch. Variables declared and never used. Equality checks with
==instead of===. Imported names you never reference. Awaiting a function that isn'tasync. The code runs, but it's almost certainly wrong. -
Style nitpicks that don't affect behavior at all. Tabs vs spaces. Semicolons or no. Trailing comma after the last array element. Single vs double quotes. Where the opening brace goes. None of this matters to the JavaScript engine, all of it matters to the reviewer who has to scroll past it.
These are different problems. They need different fixes. So they're two different tools.
- ESLint = a robot that reads your code and flags problem (1) — things that are real-world wrong or risky.
- Prettier = a robot that rewrites your code so problem (2) literally cannot happen — there's only one valid format, applied automatically.
Together they get every PR into a state where the reviewer can focus on logic, not whitespace.
Figure 1 — Two different jobs, two tools. ESLint is the bug-and-pattern checker. Prettier is the whitespace-and-quotes auto-formatter. Run them in that order and the diff a reviewer reads is just real logic.
What ESLint Actually Catches
ESLint is a static analyzer: it parses your JavaScript into a tree without running it and then walks the tree looking for patterns that match its rules. There are ~300 built-in rules and thousands of plugin rules. You don't memorise them — you pick a preset and only learn the names of the ones you hit.
Here's a flavour of what the default-recommended ruleset catches.
| Rule | Catches | Example |
|---|---|---|
no-unused-vars | Variables you declared and never read | const x = 5; then never using x |
no-undef | Variables that aren't defined anywhere | Typo: consle.log() instead of console.log() |
prefer-const | let for a binding that's never reassigned | let user = await get(); when you never write user = … |
eqeqeq | == instead of === | if (id == 1) — coerces "1" to 1, almost never what you want |
no-shadow | Inner scope reusing an outer name | const user = …; users.map(user => user.id) — the inner user hides the outer |
no-async-promise-executor | new Promise(async () => …) | A footgun where thrown errors disappear |
react-hooks/exhaustive-deps | A React effect missing a dep | The classic "stale closure" bug |
You can see why these matter — each one is a real bug class, not a style preference. The JavaScript engine will happily run all of them.
A 30-second taste
Drop this file into a fresh project and run npx eslint .:
// src/example.js
import { unused } from "./other.js";
let count = 0; // never reassigned
function add(a, b) {
const result = a + b;
return reuslt; // typo, but JavaScript will only fail at runtime
}
console.log(add(1, 2));You'll get something like:
src/example.js
1:10 error 'unused' is defined but never used no-unused-vars
3:1 error 'count' is never reassigned. Use const prefer-const
7:10 error 'reuslt' is not defined no-undef
✖ 3 problems (3 errors, 0 warnings)The typo on line 7 would have crashed at runtime — ESLint catches it before you even save. That's the value proposition.
What Prettier Actually Does
Prettier is the opposite of opinionated — it has exactly one opinion: there is one correct way to format every JavaScript construct, and Prettier knows it. You give it source, it gives you back the same source, reformatted.
It rewrites:
- Indentation (defaults to 2 spaces, configurable to tabs or 4 spaces)
- Quotes (defaults to double, configurable to single)
- Semicolons (on by default, off optionally)
- Line length (defaults to 80, will wrap long lines automatically)
- Object/array formatting (one line if short, multi-line if long, trailing commas)
- Spaces around operators, after commas, inside braces
- Where the opening
{goes (always same line)
It does not touch logic. It will never rename a variable, never reorder statements, never remove dead code. It's a pure pretty-printer.
Here's the same function before and after npx prettier --write src/example.js:
// Before — perfectly legal JS, hard to scan
function foo(a,b,c){const result={x:a,
y:b,z:c}; return result}
// After — same logic, normalised whitespace
function foo(a, b, c) {
const result = { x: a, y: b, z: c };
return result;
}Run it once, the entire file is normalised. Run it again, nothing changes — Prettier is idempotent.
That idempotency is the whole point. Once everyone in the project runs Prettier on save, the diff in any PR is only the meaningful edits. No "Alice uses tabs", no "Bob hates semicolons", no whitespace-only commits. Code review becomes about the code.
The opinion you can't override
The biggest objection to Prettier is "but I like my style better". Prettier's answer is: doesn't matter. The value isn't that its style is best, it's that everyone uses the same style without arguing. The five minutes a week you used to spend fiddling with alignment, multiplied by every developer on the project, multiplied by every PR review — that's what Prettier buys back. Style preferences are real but they're the cheapest thing to give up.
Why They're Two Tools, Not One
A reasonable question: ESLint also has stylistic rules (indent, quotes, semi). Why not just use ESLint for everything?
The historical reason: ESLint's stylistic rules are checkers — they flag a problem and you fix it. Prettier is a fixer — it just rewrites the code. Once you have an auto-fixer for whitespace, the checker is redundant noise.
So the modern split is:
- ESLint turns off all its stylistic rules and focuses on logic problems it can detect with static analysis.
- Prettier handles every layout question by rewriting the file.
You wire them so they don't fight each other (eslint-config-prettier disables the stylistic ESLint rules that would conflict), and you're done.
Figure 2 — The split-of-concerns that took the JavaScript community ~5 years to settle. Prettier owns layout because rewriting beats checking. ESLint owns semantics because static analysis is what it's good at.
Installing Both — The 5-Minute Setup
In a Next.js or vanilla Node project, here's the minimal install. Skip if the project already has these.
npm install --save-dev \
eslint \
prettier \
eslint-config-prettierThen add an eslint.config.js at the project root:
// eslint.config.js
import js from "@eslint/js";
import prettier from "eslint-config-prettier";
export default [
js.configs.recommended,
prettier, // turns off ESLint rules that conflict with Prettier
{
languageOptions: {
ecmaVersion: 2024,
sourceType: "module",
},
rules: {
// your project-specific overrides go here
},
},
];And a .prettierrc (any of .prettierrc.json, .prettierrc.js, or a "prettier" key in package.json works):
{
"semi": true,
"singleQuote": false,
"trailingComma": "es5",
"printWidth": 100
}Then add scripts to package.json so other humans (and CI) can run them:
{
"scripts": {
"lint": "eslint .",
"format": "prettier --write .",
"format:check": "prettier --check ."
}
}Now npm run lint walks every JS file and reports problems. npm run format rewrites every JS file with Prettier. npm run format:check checks the files are already formatted without rewriting them — that's the version you want in CI.
Wiring Into VS Code (or Cursor, or Zed)
The setup above runs the tools from the terminal. The real productivity unlock is running them as you type and on every save.
Install two VS Code extensions:
- ESLint (
dbaeumer.vscode-eslint) - Prettier - Code formatter (
esbenp.prettier-vscode)
Then add a .vscode/settings.json at the project root so every contributor gets the same behaviour:
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
}
}What that gives you:
- On save: Prettier rewrites the file. The diff in your PR is normalised before it ever leaves your machine.
- On save: ESLint auto-fixes anything fixable (unused imports,
let→const, etc). - As you type: ESLint underlines bugs in red. Hover for the rule name, click the lightbulb for a one-key fix.
The first time a teammate clones the repo, the workspace prompts them to install both extensions because they're listed in .vscode/extensions.json (worth adding too). Onboarding is a single "yes".
Wiring Into CI — The 3-Line Addition to Ch 15
Take the workflow from Ch 15 and add two steps:
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
- run: npm ci
- run: npm run lint # ← ESLint
- run: npm run format:check # ← Prettier in check mode
- run: npm test
- run: npm run buildThat's it. If somebody opens a PR with an unused import, lint fails red. If somebody pushes a file they forgot to format, format:check fails red. Branch protection (also Ch 15) means the PR can't merge until both go green.
The format:check step is important. Don't run prettier --write in CI — that would silently fix the file on the CI machine and the human would never know they had unformatted code. Use --check so the build fails loudly.
Figure 3 — Lint and format checks slot into the same CI pipeline from Ch 15. Each is a gate. Any single red step blocks the merge. The PR author sees exactly which check broke, fixes it, pushes again, CI re-runs.
TypeScript Adds One More Layer
If the project is TypeScript (a tsconfig.json exists), there are three tools, not two:
- TypeScript compiler (
tsc) — type errors. "You passed a string where I expected a number." - ESLint with
@typescript-eslint/parser— bugs and patterns on top of the type system. "This narrowing is suspicious." - Prettier — formatting. Same as before.
The CI just gets one more step:
- run: npx tsc --noEmit # type-check without producing output
- run: npm run lint
- run: npm run format:check
- run: npm test
- run: npm run build--noEmit is the canonical "I just want to check, don't write any .js files". You only need tsc to produce output when you're building a library — when the build step is something like Next.js or Vite, it does its own TypeScript compilation, and you only use tsc --noEmit as a check.
What These Tools Do Not Do
A common misunderstanding worth heading off:
- They don't catch logic bugs.
add(2, 3)returning6is a logic bug; both tools think it's fine. That's what unit tests are for (Ch 17). - They don't catch runtime errors that depend on inputs. "What if
userisnullhere?" — TypeScript catches it if you've typed it, otherwise nobody does until you hit it in production. - They don't catch security issues. ESLint plugins like
eslint-plugin-securitycover some, but for real SAST you want a dedicated scanner. - They don't catch dependency vulnerabilities.
npm auditor Dependabot does that.
ESLint + Prettier is the cheapest layer of the testing pyramid: it catches a real class of bug, takes one afternoon to set up, and you reap the reward on every PR forever. Tests sit on top. Type-checks too. Each layer catches a different class of mistake.
Mental Model — Three Sentences
- ESLint is the bug-and-pattern checker: it parses your JavaScript and flags real problems (unused vars, typos, equality footguns) using a configurable ruleset.
- Prettier is the auto-formatter: it rewrites whitespace, quotes, and line wrapping to one canonical style so nobody argues about it in PRs.
- In CI, you run both —
eslint .andprettier --check .— as gates before tests and build, so a PR with style drift or a flagged pattern can't merge.
If those sit in your head, every config file you'll ever see is variations on the same recipe.
Try It Yourself (10 Minutes)
The fastest way to internalise this: add both to a side project.
- In any JS/TS repo, run the install command from the "5-minute setup" section.
- Create
eslint.config.jsand.prettierrcexactly as shown. - Add the
lint,format,format:checkscripts topackage.json. - Run
npm run formatonce to normalise everything. Commit the (large) diff: "chore: apply Prettier formatting". - Run
npm run lint. Fix oreslint-disablewhatever it complains about. - Open
.vscode/settings.jsonand paste the format-on-save block. - Now make a deliberate typo, save the file, and watch your editor underline it before you even run anything.
From that point, every save normalises your code and every PR you open will be argument-free on style. That's the whole purpose of these tools.
Where This Lands in the Series
Ch 15 introduced CI and the npm run lint step appeared in the example workflow with no explanation. This chapter filled in that gap. The next chapter does the same for the npm test step — what a test actually is, why Vitest, and how to write your first one.
Ship your apps faster
When you're ready to publish your Swift app to the App Store, Simple App Shipper handles metadata, screenshots, TestFlight, and submissions — all in one place.
Try Simple App Shipper