Linting
Date: 2026-08-17
Static analysis that flags patterns you’ve decided to avoid. Its real value isn’t catching bugs — it’s encoding team decisions so they stop being argued in review, which means every rule should trace back to a decision somebody actually made.
A linter parses source into an abstract syntax tree and applies rules to it, reporting violations. ESLint is the standard for JavaScript and TypeScript.
What it can and can’t see
CAN CAN'T
unused variables is the logic correct?
undefined variables is this the right approach?
`==` instead of `===` will this be slow?
missing await is the naming good?
accessibility issues in JSX does it meet the requirement?
unreachable code
banned imports
Linting is pattern matching, not understanding. It catches classes of mistake reliably and says nothing about whether the code does the right thing — which is what review is for — Code Review.
Rules are team decisions
The framing that makes a config coherent:
"no-console": "error"
← we decided not to ship console
logging
"no-restricted-imports": [{
"paths": [{
"name": "lodash",
"message": "Use lodash-es for
tree-shaking"
}]
}]
← we decided this, and the linter
now explains it at the point of
the mistake
A rule with a custom message is documentation delivered at exactly the right moment — better than a wiki page nobody reads, because it appears in the editor while the person is making the decision.
The corollary: every enabled rule should trace to a decision. A config copied from a template contains rules nobody agreed to, which is how linting becomes something people disable.
Linting versus formatting
The distinction that removes most of the friction:
| Linting | Formatting | |
|---|---|---|
| Question | Is this a problem? | How should this look? |
| Examples | Unused vars, missing await | Indentation, quotes, semicolons |
| Debatable | Sometimes | Never — automate it |
Turn off every stylistic rule in the linter and let a formatter own them. Historically ESLint did both, and the overlap produced conflicting rules and slow lint runs — Formatting.
The rules worth having
Beyond a recommended baseline:
eslint-plugin-jsx-a11y— catches a genuine subset of accessibility issues at authoring time — WCAGno-floating-promises(TypeScript) — an unawaited promise that swallows its own failure — Async Modelsno-restricted-imports— architectural boundaries, enforcedexhaustive-depsfor React hooks — catches a real and common bug class- Custom rules for your own conventions. Writing one is more approachable than it sounds, and it’s the difference between a convention that holds and one that erodes
Where it goes wrong
- Too many rules. Hundreds of warnings nobody reads. Warnings that are never fixed should be errors or deleted — a persistent warning count trains people to ignore output
- Rules nobody agreed to, inherited from a config
- Linting formatting, producing conflicts and noise
// eslint-disablewithout a reason. Require a comment explaining why, or the disables accumulate silently- Slow enough to skip. If linting the repository takes two minutes locally, people stop running it
- Only in CI. Feedback arrives after the push, which is the wrong moment — Pre-Commit Hooks
Where it should run
EDITOR instantly, as you type
← by far the most valuable
PRE-COMMIT on changed files only, fast
— Pre-Commit Hooks
CI the whole repository,
authoritative
— Continuous Integration
See: Pre-Commit Hooks · Continuous Integration
All three, with the same config. A rule that fails in CI but not in the editor is a rule people discover at the worst moment.
Adopting it on an existing codebase
Turning on a strict config over a large repository produces thousands of errors, and the usual outcome is that nobody fixes them.
# capture existing violations as accepted
eslint --format json > .eslint-baseline.jsonRatchet rather than boil the ocean: fail only on new violations, fix the backlog opportunistically, and tighten as the count falls. A rule enforced on new code is worth far more than a rule everyone has learned to ignore.