| | |

ESLint 01 🛡️ Why ESLint — A linter, not a formatter

⏱️ Estimated read: 11 min | |

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

ConcernESLintFormatter (Prettier)
WhitespaceNo (frozen)Yes
SemicolonsNo (frozen)Yes
Quote styleNo (frozen)Yes
Unused variablesYesNo
Undefined variablesYesNo
Missing awaitYes (with types)No
React hook dependenciesYes (plugin)No
Custom domain rulesYesNo

Rule severity levels

LevelValueEffect on exit code
off0Rule disabled
warn1Reported, does not fail CI
error2Reported, exits with code 1

The 2020 policy

DecisionDetail
Stylistic rulesFrozen, no new options
Formatting rulesNot recommended; use Prettier
Core focusLogical 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

PitfallWhy It HappensFix
ESLint reports formatting issuesFormatting rules still enabledAdd eslint-config-prettier last in config
CI passes despite warningsRules set to warn, not errorSet critical rules to error
ESLint does not catch a type errorLinters are not type checkersUse TypeScript for type checking
Custom rule does not workRule API version mismatchCheck ESLint version compatibility
Slow lint timesToo many rules, or typed linting on large filesEnable 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

ItemValue
CreatedJune 2013 by Nicholas C. Zakas
PurposePluggable JavaScript linting
Core philosophyEverything is a rule, every rule is pluggable
Formatting rulesFrozen since 2020
Recommended formatterPrettier (separate tool)
Conflict avoidanceeslint-config-prettier
Rule severityoff, warn, error
Primary valueLogical 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-prettier disables 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.

Was this helpful?


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!