ESLint 01 🛡️ Why ESLint — A linter, not a formatter
ESLint is a static analysis tool for JavaScript. It parses code into an abstract syntax tree, evaluates that tree against a configurable set of rules, and reports violations. It does not reprint your program from scratch. It does not decide where line breaks go or whether strings use single or double quotes. It finds patterns that are likely to be bugs, or that violate a team’s agreed-upon conventions, and it tells you where they are.
This chapter explains why ESLint exists as a category distinct from formatters like Prettier, what “pluggable” actually means in its architecture, and why the JavaScript ecosystem has spent the last several years deliberately removing formatting responsibilities from linters. It covers the philosophy behind ESLint’s rule model, the 2020 policy shift that froze stylistic rules, and the correct division of labor between a linter that catches bugs and a formatter that enforces visual consistency.
Key point: ESLint was created to let developers write their own linting rules, not to format code. Its value comes from extensibility and logical analysis, not from whitespace decisions.
Why ESLint exists
The JavaScript error problem. JavaScript is dynamically typed and loosely structured. Without a compilation step, many errors only surface at runtime, in production, in front of users. ESLint was created in June 2013 by Nicholas C. Zakas to find problematic patterns without executing the code . A missing variable declaration, an unused parameter, a return statement inside a finally block—these are the kinds of issues a linter catches before the code ever runs.
The pluggability problem. Existing tools like JSLint and JSHint had fixed rule sets. Zakas wanted a linter where every rule was a plugin, where teams could write their own rules and load them at runtime . This design decision is why ESLint became the standard: it does not prescribe a coding style. It provides an API for defining what “correct” means, and lets projects decide.
The formatting confusion problem. For years, ESLint shipped with stylistic rules: semi, quotes, indent, max-len. Teams used these rules to enforce formatting, which worked poorly. Linters run rules in isolation, and one rule’s fixer can conflict with another’s. The typescript-eslint team documents this explicitly: lint rule fixers can conflict, forcing the linter to run additional cycles, making formatting through a linter “much slower” and “less consistent” than a purpose-built formatter .
The 2020 policy shift. ESLint’s own team recognized the problem. In a 2020 blog post, they announced that stylistic rules were frozen: “we won’t be adding any more options to stylistic rules. We’ve learned that there’s no way to satisfy everyone’s personal preferences” . This was a deliberate retreat from formatting. The linter’s future was logical analysis and bug detection, not whitespace.
a. The pluggable rule model
Everything in ESLint is a rule, and every rule is pluggable. The bundled rules follow the same API as custom rules. A rule can be turned off, set to warn, or set to error, independently of every other rule . This is not a minor implementation detail; it is the core philosophy.
// eslint.config.js — a rule is just a module that exports a create function
export default [
{
rules: {
"no-unused-vars": "error", // logical rule: unused variable
"no-undef": "error", // logical rule: undefined variable
},
},
];
The no-unused-vars rule examines the AST for variable declarations that are never referenced. The no-undef rule checks whether an identifier is declared in scope. Neither rule cares about whitespace. Neither rule will reformat your file. They report locations and, optionally, provide fixes that correct the specific logical issue.
The pluggability means a team can add a rule for their framework (eslint-plugin-react), their language extension (@typescript-eslint/eslint-plugin), or their internal conventions. The rule API is the extension point, and it is the same API the bundled rules use.
b. Logical rules versus formatting rules
The typescript-eslint documentation draws a clear line between three categories: logical, stylistic, and formatting .
Logical rules care about runtime behavior. A missing await inside an async function changes what the code does. An == comparison instead of === can produce unexpected type coercion. These are the rules ESLint is designed for.
Stylistic rules affect how code reads without changing behavior: naming conventions, preferring arrow functions over function declarations, choosing between let and const. These are debatable but still concern code structure.
Formatting rules care only about trivia: semicolons, whitespace, indentation, line breaks. These are the rules that conflict with Prettier. They are the rules ESLint has frozen and the typescript-eslint team refuses to accept new additions for .
// A logical issue ESLint catches:
async function load() {
const data = fetch("/api"); // missing await
return data.json(); // data is a Promise, not the response
}
// A formatting issue ESLint should NOT handle:
const x = {a:1,b:2}; // spacing — Prettier's job
The first example is a bug. The second is a visual preference. ESLint’s value is in the first.
c. What “pluggable” enables that formatters cannot
A formatter like Prettier makes decisions about whitespace and line breaks. It does not know that you forgot to await a promise. It does not know that a variable is shadowing an outer scope. It cannot warn you that a React hook is called conditionally.
ESLint’s rule model allows analysis that requires understanding code semantics, not just syntax. The no-floating-promises rule (from typescript-eslint) tracks whether a Promise-returning expression is handled. The react-hooks/exhaustive-deps rule analyzes dependency arrays against the variables actually referenced inside the effect. These rules require type information and control-flow analysis. They are not formatting decisions; they are correctness checks.
// react-hooks/exhaustive-deps flags this:
useEffect(() => {
fetchUser(userId); // userId is used but not in dependency array
}, []); // ESLint: missing dependency 'userId'
// Prettier formats this code perfectly.
// ESLint tells you it is wrong.
A formatter that reprinted the AST would produce identical output regardless of whether the dependency array was correct. ESLint’s value is in knowing the difference.
d. The separation of concerns in practice
The modern JavaScript toolchain has converged on a division of labor. Formatters handle formatting. Linters handle logic. The typescript-eslint team recommends using eslint-config-prettier to disable any formatting rules that might still be enabled in an ESLint configuration, so the two tools do not conflict .
// eslint.config.js — disable formatting rules, keep logical rules
import eslintConfigPrettier from "eslint-config-prettier";
export default [
// ... your rules
eslintConfigPrettier, // turns off all formatting rules
];
This configuration does not disable ESLint. It disables the subset of ESLint rules that duplicate Prettier’s job. The logical rules—the rules that catch bugs—remain active. The formatter runs separately, either on save in the editor or in a pre-commit hook, and the linter runs in CI to enforce correctness.
Complete Example Session
// ============================================
// PART 1: A LOGICAL RULE IN ACTION
// ============================================
// no-unused-vars: an ESLint logical rule
function calculate(a, b) {
const result = a + b;
return a; // result is unused
}
// ESLint reports: 'result' is assigned a value but never used.
// Prettier would format this code without complaint.
// ============================================
// PART 2: A RULE THAT REQUIRES SEMANTICS
// ============================================
// no-floating-promises: requires type information
async function save() {
database.write(data); // returns a Promise, never awaited
}
// ESLint flags the floating promise.
// This is not a formatting issue; it is a bug.
// ============================================
// PART 3: A FORMATTING RULE (DEPRECATED)
// ============================================
// semi: a formatting rule frozen by ESLint
const x = 1 // missing semicolon
// ESLint's semi rule would flag this.
// But ESLint's team froze stylistic rules in 2020.
// Use Prettier for semicolons instead.
// ============================================
// PART 4: THE CONFIGURATION SHAPE
// ============================================
// eslint.config.js (flat config)
export default [
{
rules: {
"no-unused-vars": "error",
"no-undef": "error",
"no-duplicate-imports": "error",
},
},
];
// Each rule is independent. Each can be off, warn, or error.
// No rule is "too important to turn off" .
// ============================================
// PART 5: PLUGINS EXTEND THE RULE SET
// ============================================
import react from "eslint-plugin-react";
import reactHooks from "eslint-plugin-react-hooks";
export default [
react.configs.recommended,
{
plugins: { "react-hooks": reactHooks },
rules: {
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn",
},
},
];
// react-hooks/exhaustive-deps analyzes dependency arrays.
// A formatter cannot do this. It requires understanding React semantics.
// ============================================
// PART 6: DISABLING FORMATTING RULES
// ============================================
import eslintConfigPrettier from "eslint-config-prettier";
export default [
// logical rules...
eslintConfigPrettier, // disables formatting rules only
];
// The config exclusively turns OFF rules .
// Logical rules remain active.
// ============================================
// PART 7: A CUSTOM RULE
// ============================================
// ESLint's primary creation reason was custom rules .
// Every project can define rules specific to its domain.
// Example: a rule that forbids console.log in production code
export default {
rules: {
"no-console-log": {
create(context) {
return {
MemberExpression(node) {
if (node.object.name === "console" && node.property.name === "log") {
context.report({ node, message: "No console.log in production." });
}
},
};
},
},
},
};
// ============================================
// PART 8: ESLINT IS NOT A FORMATTER
// ============================================
// ESLint parses, checks rules, reports, and optionally fixes.
// It does not reprint the entire file.
// A file with no rule violations is left exactly as written.
const messy = {a:1,b:2}; // no logical violation
// ESLint: no errors.
// Prettier: reformats to { a: 1, b: 2 }.
// ============================================
// PART 9: THE RECOMMENDED WORKFLOW
// ============================================
// 1. Prettier formats on save (or pre-commit).
// 2. ESLint runs in CI to catch bugs.
// 3. eslint-config-prettier prevents conflicts.
//
// Two tools. Two jobs. No overlap.
// ============================================
// PART 10: WHAT ESLINT IS NOT
// ============================================
// Not a type checker (that is TypeScript's job).
// Not a bundler (that is Vite or webpack).
// Not a formatter (that is Prettier's job).
// It is a pluggable static analysis tool for finding problems.
The ten parts covered logical rules, semantic rules, the deprecated formatting category, configuration shape, plugins, conflict avoidance, custom rules, the non-formatting nature of ESLint, the recommended workflow, and the boundary of its responsibilities.
Quick Reference
ESLint versus formatter
| Concern | ESLint | Formatter (Prettier) |
|---|---|---|
| Whitespace | No (frozen) | Yes |
| Semicolons | No (frozen) | Yes |
| Quote style | No (frozen) | Yes |
| Unused variables | Yes | No |
| Undefined variables | Yes | No |
Missing await | Yes (with types) | No |
| React hook dependencies | Yes (plugin) | No |
| Custom domain rules | Yes | No |
Rule severity levels
| Level | Value | Effect on exit code |
|---|---|---|
off | 0 | Rule disabled |
warn | 1 | Reported, does not fail CI |
error | 2 | Reported, exits with code 1 |
The 2020 policy
| Decision | Detail |
|---|---|
| Stylistic rules | Frozen, no new options |
| Formatting rules | Not recommended; use Prettier |
| Core focus | Logical analysis and extensibility |
Best Practices
✅ Do This:
// Use ESLint for logical rules
"no-unused-vars": "error" // ✅
// Use plugins for framework-specific logic
"react-hooks/exhaustive-deps": "warn" // ✅
// Disable formatting rules with eslint-config-prettier
eslintConfigPrettier // ✅
// Write custom rules for domain-specific checks
"no-console-log": { create(context) { /* ... */ } } // ✅
❌ Don’t Do This:
// Use ESLint to enforce semicolons or quotes
" semi": "error", "quotes": "error" // ❌ (frozen)
// Run Prettier inside ESLint for performance
"prettier/prettier": "error" // ❌ (slow)
// Expect ESLint to catch type errors
"no-undef": "error" // does not replace TypeScript // ❌
// Treat warn and error as interchangeable in CI
"no-unused-vars": "warn" // CI will not fail // ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| ESLint reports formatting issues | Formatting rules still enabled | Add eslint-config-prettier last in config |
| CI passes despite warnings | Rules set to warn, not error | Set critical rules to error |
| ESLint does not catch a type error | Linters are not type checkers | Use TypeScript for type checking |
| Custom rule does not work | Rule API version mismatch | Check ESLint version compatibility |
| Slow lint times | Too many rules, or typed linting on large files | Enable type-aware rules selectively; use caching |
Real-World Examples
1. Logical rule: no-unused-vars
"no-unused-vars": "error"
2. Logical rule: no-undef
"no-undef": "error"
3. Type-aware rule: no-floating-promises
"@typescript-eslint/no-floating-promises": "error"
4. React hook rule
"react-hooks/exhaustive-deps": "warn"
5. Disable formatting conflicts
import eslintConfigPrettier from "eslint-config-prettier";
export default [eslintConfigPrettier];
6. Custom rule registration
rules: { "my-rule": myRuleModule }
7. Warn level for non-critical rules
"no-console": "warn"
8. Error level for CI-blocking rules
"no-debugger": "error"
9. Plugin configuration
plugins: { react: reactPlugin }
10. Verify config resolution
npx eslint --print-config src/index.js
Visual
Linter versus formatter pipeline
┌─────────────────────────────────────────────────────────────┐
│ SOURCE CODE │
│ │ │
│ ├──────────────────────┬──────────────────────┐ │
│ ▼ ▼ ▼ │
│ PRETTIER ESLINT TYPECHECK │
│ (formatter) (linter) (tsc) │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ Reprints AST Checks rules Checks types │
│ for consistency for logic bugs for correctness│
│ │ │ │ │
│ ▼ ▼ ▼ │
│ FORMATTED CODE BUG REPORTS TYPE ERRORS │
│ │
│ Three tools. Three responsibilities. No overlap. │
└─────────────────────────────────────────────────────────────┘
ESLint rule categories
┌─────────────────────────────────────────────────────────────┐
│ RULE CATEGORIES (2020 policy) │
│ │
│ LOGICAL (ESLint's focus) │
│ no-unused-vars, no-undef, no-duplicate-imports │
│ no-floating-promises (with types) │
│ react-hooks/exhaustive-deps │
│ → Catch bugs, prevent runtime errors │
│ │
│ STYLISTIC (frozen) │
│ prefer-const, no-var, func-style │
│ → Code structure, not formatting │
│ │
│ FORMATTING (removed) │
│ semi, quotes, indent, max-len │
│ → Prettier's responsibility │
└─────────────────────────────────────────────────────────────┘
The separation workflow
┌─────────────────────────────────────────────────────────────┐
│ RECOMMENDED WORKFLOW │
│ │
│ Developer writes code │
│ │ │
│ ▼ │
│ Editor save ──► Prettier --write (formatting) │
│ │ │
│ ▼ │
│ Git commit ──► ESLint --fix (logical fixes) │
│ │ │
│ ▼ │
│ CI pipeline ──► ESLint --max-warnings 0 (gate) │
│ │ │
│ ▼ │
│ Merge │
│ │
│ Prettier handles appearance. │
│ ESLint handles correctness. │
└─────────────────────────────────────────────────────────────┘
The 2020 policy shift
┌─────────────────────────────────────────────────────────────┐
│ ESLINT'S EVOLUTION │
│ │
│ 2013-2019: │
│ Linter + formatting rules + stylistic rules │
│ Formatting through ESLint was common │
│ │
│ 2020: │
│ Stylistic rules frozen │
│ Formatting rules discouraged │
│ Team acknowledges: cannot satisfy everyone's preferences │
│ │
│ 2024+: │
│ ESLint focuses on logical analysis │
│ Prettier handles formatting │
│ eslint-config-prettier bridges the two │
└─────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Created | June 2013 by Nicholas C. Zakas |
| Purpose | Pluggable JavaScript linting |
| Core philosophy | Everything is a rule, every rule is pluggable |
| Formatting rules | Frozen since 2020 |
| Recommended formatter | Prettier (separate tool) |
| Conflict avoidance | eslint-config-prettier |
| Rule severity | off, warn, error |
| Primary value | Logical analysis and custom rules |
Key takeaways:
- ESLint is a linter, not a formatter. It analyzes the AST for logical problems and custom rule violations. It does not reprint files from scratch.
- Pluggability is the founding principle. Every rule, bundled or custom, uses the same API. Teams can define what “correct” means for their domain .
- Formatting rules were frozen in 2020. The ESLint team acknowledged that linters are slower and less consistent than purpose-built formatters for whitespace decisions .
- The correct toolchain separates concerns. Prettier formats; ESLint catches bugs; TypeScript checks types.
eslint-config-prettierdisables the overlapping rules. - ESLint’s value is in rules a formatter cannot replicate. Missing
await, unused variables, React hook dependencies, custom domain rules—these require semantic analysis, not reprinting.
Remember: ESLint was built to let developers write their own linting rules, not to enforce a style guide through formatting. The ecosystem spent a decade learning that linters make poor formatters: rule fixers conflict, performance degrades, and edge cases multiply. The 2020 policy shift was an acknowledgment, not a retreat. Use ESLint for the problems it was designed to find—logical errors, suspicious patterns, framework-specific mistakes. Use a formatter for the visual consistency it was designed to provide. The separation is not a compromise; it is the correct architecture.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!