NodeJSLinting with ESLint

Linting with ESLint

A linter statically analyzes your code for problems without running it — likely bugs, suspect patterns, and style violations — and ESLint is the standard linter for JavaScript and TypeScript. It catches things the language won't: an unused variable, an unhandled promise, a == where you meant ===, a missing await, a forgotten break. Configured with shared rule sets and run in your editor and CI, it keeps a codebase consistent and bug-resistant across a whole team. This page covers what linting is (vs formatting), the flat config, rules and severities, autofix, and integrating ESLint into your workflow.

Linting vs formatting

Linter (ESLint)

Formatter (Prettier)

Concerned with

Code correctness & quality

Code appearance

Catches

Bugs, bad patterns, unused vars, no-eval

Indentation, quotes, line width, commas

Can change meaning?

Yes (some autofixes alter logic)

No — only whitespace/syntax style

Example complaint

"result is assigned but never used"

"Expected 2 spaces, found 4"

ESLint checks correctness and patterns; a formatter only restyles whitespace — use both, for different jobs
It's worth separating two tools people often conflate. A **linter** analyzes code for *correctness and quality* — potential bugs (unhandled promises, shadowed variables), dangerous patterns (`eval`, accidental globals), and maintainability issues (unused code, overly complex functions). A **formatter** like [Prettier](/nodejs/prettier) only cares about *appearance* — indentation, quote style, line length, trailing commas — and never changes what the code *does*. Modern practice is to use **both** and let them specialize: Prettier owns all formatting, ESLint owns code quality (with formatting-related ESLint rules turned off so the two don't fight). ESLint can do some stylistic checking, but delegating that to Prettier and keeping ESLint focused on real problems is cleaner and avoids conflicts.
Setup and flat config

Bash
npm install --save-dev eslint
npx eslint --init        # interactive setup: picks plugins, creates the config

eslint.config.js (flat config — the modern format)

JS
import js from '@eslint/js'

export default [
  js.configs.recommended,            // ESLint's recommended baseline rules
  {
    files: ['**/*.js'],
    languageOptions: {
      ecmaVersion: 2023,
      sourceType: 'module',
      globals: { process: 'readonly', console: 'readonly' },  // Node globals
    },
    rules: {
      'no-unused-vars': 'warn',
      'no-console': 'off',
      eqeqeq: 'error',               // require === over ==
      'no-await-in-loop': 'warn',
    },
  },
]
Modern ESLint uses flat config (`eslint.config.js`) — an array of config objects with files, language options, and rules
As of ESLint 9 the configuration format is **flat config** — an `eslint.config.js` file exporting an **array** of configuration objects (replacing the older `.eslintrc` JSON/YAML system). Each object can scope itself with `files` globs and set `languageOptions` (ECMAScript version, module type, known globals like `process`), `plugins`, and `rules`. You almost always start from a **shared base** — `js.configs.recommended`, the [`typescript-eslint`](/nodejs/typescript-intro) configs, or a published style guide like Airbnb — and then override individual rules. Composing configs as an array makes it explicit which rules apply to which files (e.g. relax rules for test files, add Node globals for server code). `npx eslint --init` scaffolds a sensible starting config for you based on a few questions.
Rules and severity levels

JS
rules: {
  'no-debugger': 'error',     // 2 — fails the lint (non-zero exit) → blocks CI
  'no-unused-vars': 'warn',   // 1 — reported, but does NOT fail the build
  'no-console': 'off',        // 0 — rule disabled
}

// Disable a rule inline for a justified exception (use sparingly):
// eslint-disable-next-line no-console
console.log('intentional startup banner')
Each rule is `off`/`warn`/`error` — only `error` fails the build; disable inline only with a real reason
Every ESLint rule has one of three **severities**: `'off'` (`0`, disabled), `'warn'` (`1`, reported but doesn't fail), and `'error'` (`2`, reported *and* causes a non-zero exit code that fails CI). Use **`error`** for things that should block a merge (likely bugs, `debugger` left in, `eqeqeq`) and **`warn`** for advisory issues you want visible but not blocking during a transition. You can override a rule for a single line with an **inline disable comment** (`// eslint-disable-next-line rule-name`) — fine for a genuine, justified exception, but treat each one as a small smell: a config littered with disables usually means a rule is wrong for the project (better to turn it off centrally) or the code should change. Keep `error`-level rules meaningful so a red lint always means "fix this."
Autofix and CI integration

package.json scripts

JSON
{
  "scripts": {
    "lint": "eslint .",            // check everything (use in CI — fails on errors)
    "lint:fix": "eslint . --fix"   // auto-fix everything ESLint safely can
  }
}
`--fix` rewrites your files — many rules autofix safely, but review the changes; run `eslint .` in CI to enforce the rules
Many ESLint rules are **auto-fixable**: `eslint . --fix` rewrites files to resolve them (sorting imports, removing unneeded code, adding missing `===`), which clears a lot of findings instantly. It's a real time-saver, but `--fix` **modifies your source**, so run it on a clean working tree and review the diff — most fixes are mechanical and safe, yet a few rules can subtly change behavior, and you want to *see* what changed. The complementary half is **enforcement**: run plain `eslint .` in **CI** so any `error`-level violation fails the build and can't be merged, and run ESLint in your **editor** for instant inline feedback as you type. Combined with a [pre-commit hook](/nodejs/husky-lint-staged) that lints staged files, this gives layered defense — fast local feedback, a commit-time gate, and a CI backstop — so problems are caught at the cheapest possible moment.
Next
Hand all the styling to a dedicated formatter: [Formatting with Prettier](/nodejs/prettier).