Skip to content
JavaScript js tooling 4 min read

Linting & Formatting

Two tools sit at the heart of every healthy JavaScript codebase: a linter that finds suspicious code before it becomes a bug, and a formatter that makes every file look identical no matter who wrote it. ESLint and Prettier handle these jobs respectively, and they complement each other rather than overlap. Wiring them into your editor and into a pre-commit hook means style debates and whole classes of mistakes simply stop reaching code review.

Linting vs formatting

It is tempting to lump these together, but they answer different questions. A formatter only cares about how the code looks — indentation, quote style, line length — and it rewrites your file deterministically. A linter cares about what the code means — unused variables, unreachable branches, accidental reassignment, missing await. A linter can warn or error; a formatter just normalizes.

ConcernESLint (linter)Prettier (formatter)
Finds bugs (unused vars, no-undef)YesNo
Enforces code style (spacing, quotes)Possible, but defer to PrettierYes
Rewrites your fileWith --fix (partial)Always
Configurable rulesHundredsA handful
Understands semanticsYes (AST + scope)No (just reprints AST)

The modern convention is to let Prettier own all purely stylistic decisions and let ESLint own correctness. This avoids the two tools fighting over the same lines.

Setting up ESLint

ESLint 9 uses a flat config file named eslint.config.js. Install it and generate a starter config:

npm install --save-dev eslint
npx eslint --init

A minimal flat config that lints modern browser and Node code looks like this:

// eslint.config.js
import js from "@eslint/js";
import globals from "globals";

export default [
  js.configs.recommended,
  {
    files: ["**/*.js"],
    languageOptions: {
      ecmaVersion: 2024,
      sourceType: "module",
      globals: { ...globals.browser, ...globals.node },
    },
    rules: {
      "no-unused-vars": "warn",
      "no-undef": "error",
      eqeqeq: ["error", "always"],
      "prefer-const": "error",
    },
  },
];

Each rule takes a severity: "off", "warn", or "error". Run the linter from the command line, and add --fix to auto-correct the rules that support it:

npx eslint src/
npx eslint src/ --fix

Given a file with a loose equality check and an unused import, ESLint reports precisely where the problems are:

Output:

/app/src/cart.js
  3:7   warning  'formatPrice' is defined but never used  no-unused-vars
  9:14  error    Expected '===' and instead saw '=='      eqeqeq

✖ 2 problems (1 error, 1 warning)

Setting up Prettier

Prettier needs almost no configuration — that is the point. Install it and add a tiny config to record the few choices it offers:

npm install --save-dev prettier
{
  "semi": true,
  "singleQuote": true,
  "trailingComma": "all",
  "printWidth": 80
}

Save that as .prettierrc.json. Format the whole project, or just check for drift in CI:

npx prettier --write .       # rewrite files in place
npx prettier --check .       # exit non-zero if anything is unformatted

Add a .prettierignore file (same syntax as .gitignore) for build output and vendored code you do not want touched.

Making ESLint and Prettier coexist

If you enabled ESLint’s stylistic rules, they may conflict with Prettier. The fix is eslint-config-prettier, which simply turns off every ESLint rule that Prettier already handles:

npm install --save-dev eslint-config-prettier
// eslint.config.js (append to the array)
import prettier from "eslint-config-prettier";

export default [
  // ...your other config blocks
  prettier, // must come last so it can disable conflicting rules
];

Editor integration

The biggest productivity win comes from running both tools as you type. In VS Code, install the ESLint and Prettier extensions, then enable format-on-save and ESLint auto-fix in your workspace settings:

{
  "editor.formatOnSave": true,
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": "explicit"
  }
}

Now every save reformats with Prettier and applies ESLint’s safe auto-fixes. Most other editors (WebStorm, Neovim, Zed) have equivalent settings.

Pre-commit hooks

Editor integration is great, but it relies on every contributor configuring their machine. A pre-commit hook enforces the rules at the gate, where they cannot be skipped. Husky installs Git hooks and lint-staged runs your tools against only the files that are actually staged, keeping commits fast.

npm install --save-dev husky lint-staged
npx husky init

husky init creates .husky/pre-commit. Make it call lint-staged:

npx lint-staged

Then declare in package.json what should run per file type:

{
  "lint-staged": {
    "*.js": ["eslint --fix", "prettier --write"],
    "*.{json,css,md}": ["prettier --write"]
  }
}

On every commit, staged JavaScript is linted and formatted automatically; if ESLint reports an unfixable error, the commit is blocked.

Run prettier --check and eslint . in CI as well. Local hooks can be bypassed with git commit --no-verify, so CI is your real safety net.

Best Practices

  • Let Prettier own formatting and ESLint own correctness — never duplicate stylistic rules across both.
  • Always add eslint-config-prettier last so the two tools never fight over the same lines.
  • Commit your eslint.config.js and .prettierrc.json so the whole team shares one source of truth.
  • Use lint-staged to lint only changed files; full-repo linting belongs in CI, not the commit hook.
  • Treat lint warnings as a backlog, not noise — set --max-warnings 0 in CI once you are clean to prevent regressions.
  • Pin tool versions in package.json so a Prettier upgrade does not silently reformat the entire codebase mid-sprint.
Last updated June 1, 2026
Was this helpful?