Static Analysis & Formatting
|
This section documents modern ECMAScript and core browser JavaScript APIs — it is not tied to any specific framework or library (React, Vue, Angular, etc.). This content was generated with the assistance of AI and should be verified against the current ECMAScript specification and MDN documentation before relying on it in production, since JavaScript language features and browser API support continue to evolve. This section’s bibliography lists the reference material consulted while preparing these pages. |
Two tools dominate day-to-day JavaScript code quality: ESLint, a static analyzer that finds bugs and enforces coding conventions by walking a file’s parsed syntax tree, and Prettier, an opinionated code formatter that rewrites whitespace, quotes, line breaks, and punctuation to a single consistent style. They solve different problems and are almost always used together on the same project. This page covers ESLint’s rule and configuration model, a worked example of catching a real bug, Prettier’s purely-formatting role, and how the two are composed so they don’t fight each other. See also Babel & Build-Time Transforms for how these tools fit alongside a transpilation step, and Testing with Jest for the third leg of a typical JavaScript quality toolchain.
ESLint: What It Does and Why It Exists
ESLint parses source code into an abstract syntax tree (AST) and runs a set of independent rules against that
tree, each rule pattern-matching for a specific mistake or style violation — an unused variable, a comparison
using == instead of ===, a switch statement missing a break, an accidentally shadowed variable, a promise
whose rejection is never handled. This is fundamentally different from what a formatter does: ESLint reasons
about the meaning of the code (what it does, what could go wrong), not just its visual layout. Many rules — but not all — are auto-fixable, meaning ESLint knows how to safely rewrite the offending code itself rather than
just reporting it.
Install ESLint as a dev dependency and run it from the command line or from an npm script:
npm install --save-dev eslint
npx eslint . # lint every file ESLint's config says to look at
npx eslint src/app.js # lint a single file
npx eslint . --fix # apply every auto-fixable rule's fix, then report what's left
Flat Config: eslint.config.js
Since ESLint 9, the flat config format — a plain JavaScript module named eslint.config.js (or .mjs/.cjs)
at the project root — is the default and recommended way to configure ESLint, replacing the older .eslintrc.
cascading-file format. A flat config file exports an array of *configuration objects, each one applying a set of
rules, plugins, and parser options to a set of files; later objects in the array override earlier ones for any
file they both match, so the array reads top-to-bottom as "apply this, then apply this on top":
// eslint.config.js
import js from "@eslint/js";
export default [
js.configs.recommended, // ESLint's own recommended rule set
{
files: ["**/*.js"],
languageOptions: {
ecmaVersion: "latest",
sourceType: "module",
},
rules: {
"no-unused-vars": "warn", // override: downgrade from "error" to "warn"
"no-console": "off",
},
},
{
ignores: ["dist/**", "build/**", "node_modules/**"],
},
];
Each rule is set to one of three severities: "off" (or 0), "warn" (or 1, reported but does not fail the
eslint process’s exit code), or "error" (or 2, reported and causes a non-zero exit code — the setting to
use for CI). Rules that take options use a two-element array form instead of a bare string, e.g.
"no-unused-vars": ["error", { argsIgnorePattern: "^_" }] to allow deliberately-unused function parameters
prefixed with an underscore.
Recommended Rule Sets
Rather than hand-picking every rule, most projects start from a curated set and layer overrides on top, exactly
as the example above does with js.configs.recommended:
| Rule set | What it covers |
|---|---|
|
ESLint’s own core rules considered to catch genuine bugs with a low false-positive rate — |
Framework/plugin-provided sets, e.g. |
Rules specific to a language extension or framework — TypeScript type-aware checks, React hooks-usage rules, accessibility checks on JSX markup. Added as an extra entry in the flat config array alongside `@eslint/js’s recommended set. |
A plugin’s rules are namespaced under its name once imported — e.g. react-hooks/rules-of-hooks — and a
config object enables them the same way as core rules, inside that object’s rules block.
The --fix Flag
Many stylistic and some logic rules (no-extra-semi, prefer-const, no-var, curly, and dozens more) are
marked as fixable: ESLint knows a rewrite that resolves the violation without changing the code’s behavior.
Passing --fix on the command line applies every such fix in place, then reports whatever violations remain
(rules with no safe automatic fix, or fixes ESLint considers unsafe to apply blindly still get reported, not
silently changed):
npx eslint . --fix
--fix is safe to run routinely and is commonly wired into a pre-commit hook or an editor’s "format/fix on
save" action, since every fix it applies is one ESLint’s rule author judged behavior-preserving.
A Worked Example: Catching a Real Bug
The clearest way to see ESLint’s value is a rule that catches something a formatter never could, because it
requires understanding what the code does. Consider no-unused-vars, part of eslint:recommended /
`@eslint/js’s recommended set, applied to this function:
// src/discount.js
function applyDiscount(price, discountRate) {
const discountedPrice = price - price * discountRate;
const roundedPrice = Math.round(discountedPrice * 100) / 100;
return discountedPrice; // bug: returns the un-rounded value; roundedPrice is dead code
}
Running ESLint against this file:
$ npx eslint src/discount.js
/project/src/discount.js
4:9 error 'roundedPrice' is assigned a value but never used no-unused-vars
✖ 1 problem (1 error, 0 warnings)
The unused-variable warning is a symptom, not just a nuisance: it points straight at a real bug — the function
computes a rounded price and then returns the wrong variable. no-unused-vars cannot auto-fix this, because
ESLint has no way to know whether the fix is deleting the dead variable or (as here) using it — that judgment
call requires the human reading the code. The fix:
function applyDiscount(price, discountRate) {
const discountedPrice = price - price * discountRate;
const roundedPrice = Math.round(discountedPrice * 100) / 100;
return roundedPrice; // fixed: return the rounded value
}
A second common category is a dropped await. The require-await rule flags an async function that never
actually awaits anything (often a sign the async keyword was added out of habit, or that an awaited call was
accidentally left off), while type-aware rules from typescript-eslint (no-floating-promises) go further and
flag a promise-returning call whose result is neither await`ed, `return`ed, nor explicitly discarded with
`void:
async function saveUser(user) {
db.insert(user); // bug: missing `await` -- the function returns before the insert finishes
return { ok: true };
}
/project/src/users.js
2:3 error Promise returned in function argument where a void return was expected @typescript-eslint/no-floating-promises
✖ 1 problem (1 error, 0 warnings)
Both examples share the same shape: the code is syntactically valid and would run without throwing, but does the wrong thing at runtime. This is precisely the class of problem a formatter cannot detect, because it isn’t about layout — it’s about control flow and data flow, which is what makes ESLint’s AST-based analysis worth running in CI even on a project that otherwise looks clean.
Prettier: Formatting Only, No Logic
Prettier’s entire job is to take source code and re-print it in one canonical layout: consistent indentation,
quote style, line length (wrapping long lines, collapsing short ones), trailing commas, semicolon usage, and
spacing. It parses the code (using the same kind of parser ESLint uses) but only to understand its structure well
enough to re-print it — it has no concept of rules with severities, no plugin ecosystem, and, critically,
no ability or intention to detect bugs. Prettier does not know what no-unused-vars means and never will;
that is explicitly out of scope by design.
The practical benefit of that narrowness is that Prettier’s formatting is almost entirely non-configurable by
choice — a handful of options exist (printWidth, tabWidth, semi, singleQuote, trailingComma), and the
project deliberately keeps that list short so teams stop debating style and just run the tool:
npm install --save-dev prettier
npx prettier --write . # reformat every file in place
npx prettier --check . # exit non-zero if any file isn't already formatted (for CI)
{
"semi": true,
"singleQuote": true,
"trailingComma": "es5",
"printWidth": 100
}
The conceptual split to keep in mind: ESLint asks "is this code correct and does it follow our conventions?"; Prettier asks "is this code laid out consistently?". A file can be perfectly formatted by Prettier and still contain the dead-code bug shown above — formatting says nothing about behavior. Conversely, a file with zero ESLint errors can still have inconsistent quote styles or line wrapping if Prettier hasn’t touched it. Neither tool substitutes for the other.
Composing ESLint and Prettier
Because both tools parse and reason about the same source, a naive setup where ESLint also has its own
formatting-style rules turned on (from eslint:recommended-adjacent style rules, or a style-guide plugin like
eslint-config-airbnb) can produce conflicting fixes: ESLint’s --fix wants one line length or quote style,
Prettier’s --write wants another, and running both repeatedly flip-flops the file. The standard fix is not to
make Prettier smarter about linting, or ESLint smarter about formatting — it’s to have ESLint stop opining on
formatting entirely and let Prettier own that concern exclusively.
eslint-config-prettier is a config that does exactly one thing: it turns off every ESLint core (and several
popular plugins') rule that overlaps with something Prettier already controls — indentation, quotes, comma
placement, and so on — without turning on any rules of its own. Placed last in the flat config array so nothing
after it re-enables what it disabled:
npm install --save-dev eslint-config-prettier
// eslint.config.js
import js from "@eslint/js";
import eslintConfigPrettier from "eslint-config-prettier";
export default [
js.configs.recommended,
{
files: ["**/*.js"],
rules: {
"no-unused-vars": "error",
},
},
eslintConfigPrettier, // must come last: disables ESLint's own formatting-related rules
];
With this in place, ESLint is responsible purely for logic/bug rules and Prettier is responsible purely for layout, and the two tools' outputs never disagree. A typical project wires both into the same npm scripts and CI step so neither is optional:
{
"scripts": {
"lint": "eslint . && prettier --check .",
"fix": "eslint . --fix && prettier --write ."
}
}
Some setups go one step further and run Prettier itself as an ESLint rule via eslint-plugin-prettier, which
reports formatting deviations as ESLint errors (so a single eslint --fix run applies both kinds of fixes). This
is convenient for a single-command workflow but couples the two tools' execution together and is generally
slower than running Prettier directly; eslint-config-prettier plus a separate prettier --write step is the
more common and better-performing setup, and is what the official documentation for both projects currently
recommends.
Editor Integration
Both tools are typically run automatically on save via editor extensions (the ESLint and Prettier extensions for
VS Code, or equivalents for other editors), configured to run eslint --fix and prettier --write against the
active file. This surfaces bugs and formatting drift immediately rather than at commit time or in CI, though CI
should still run both eslint (without --fix, so it fails the build on any remaining error) and
prettier --check as a backstop, since editor integration is a convenience that not every contributor or every
commit path (a direct push, a generated file) goes through. A pre-commit hook (via a tool like husky combined
with lint-staged, restricting the run to staged files) is a common middle ground: fast enough to run on every
commit, catching most issues before they ever reach CI.
Summary
ESLint and Prettier solve two different, complementary problems: ESLint’s rule engine, built on the flat
eslint.config.js format and typically started from @eslint/js’s recommended rules, walks the AST to catch
real bugs and enforce logic-level conventions, with `--fix auto-applying whatever it safely can; Prettier
re-prints the same source into one consistent, non-negotiable layout with no concept of correctness at all.
eslint-config-prettier is the glue that keeps them from fighting over formatting, by turning off ESLint’s
formatting-adjacent rules and leaving Prettier as the sole authority on layout. Both configuration formats and
rule sets evolve — verify current option names and defaults against
ESLint’s official documentation and
Prettier’s official documentation before adopting a specific setup.