Every team that adopts a linter eventually holds the same meeting. Someone wants no-else-return on. Someone else has strong feelings about arrow-function bodies. Forty minutes disappear, nobody's mind changes, and the config ends up with whatever the loudest person wanted.
The short version, and the thing that ends most of these arguments: a rule earns its place if it catches something a reviewer would flag anyway, and it doesn't if the only argument for it is that someone prefers it. Correctness rules are worth defending. Taste rules aren't worth a meeting — they're worth a coin flip, a formatter, or an off.
This is a decision procedure, not a rule list. Rule catalogs change; the test for whether a rule belongs in your config doesn't.
The test a rule has to pass
Before enabling anything beyond your linter's recommended preset, ask three questions in order. A rule needs all three.
1. Would a human reviewer flag this? If yes, automating it saves real review attention and removes a source of interpersonal friction — nobody wants to be the person who comments "you left a console.log in" for the fortieth time. If a reviewer wouldn't mention it, a machine failing your build over it is a net cost.
2. What's the false-positive rate on your code? A rule that fires correctly 99% of the time is a gift. A rule that fires correctly 70% of the time trains your team to write disable comments, and once disable comments are normal, every rule in your config is weaker.
3. Is it already handled elsewhere? Formatting is handled by the formatter. Type mismatches are handled by the type checker. A lint rule that duplicates either is redundant at best and actively fighting your other tools at worst. (If those three categories still blur together for you, the linting fundamentals guide lays out where each tool's job starts and stops.)
Most rule arguments die at question one or three. The argument about no-else-return isn't really about no-else-return — it's about whether anyone would have raised it in review. They wouldn't have.
Three tiers of lint rules
Sorting your candidate rules into tiers before you argue about them is most of the work.
Tier 1: correctness rules — enable, set to error, don't negotiate
These flag code that is probably a bug. They're what earns a linter its place in your pipeline, and they should be errors from day one because their false-positive rate is low and the cost of a miss is high. Most of these live in ESLint's recommended preset already:
no-unused-vars— dead identifiers, usually the residue of a refactor that removed the use but not the declaration. Sometimes it's noise; sometimes it's a variable you forgot to actually use.no-undef— a reference to something that isn't declared anywhere in scope.no-fallthrough— aswitchcase that runs into the next one without abreak. Occasionally intentional, and the rule supports an explicit comment for that case.no-cond-assign—if (x = 5)whereif (x === 5)was meant.no-dupe-keys— the same key twice in an object literal, where the second silently wins.no-unreachable— code after areturn,throw, orbreakthat can never run.
Two more are worth adding explicitly, because they are not on by default:
eqeqeq— requires===over==, removing a whole family of surprising coercion bugs. This is the single most commonly added non-default rule for a reason: the failure mode is silent and the fix is mechanical.@typescript-eslint/no-floating-promises(TypeScript projects, and it needs type information configured) — flags promises that are created but never awaited or handled, which is how async errors disappear without a trace.
If someone argues against a Tier 1 rule, listen — they may have a codebase-specific reason, like a code-generation step that legitimately produces unused parameters. That's a targeted override with a comment explaining it, not a reason to drop the rule.
Tier 2: contested rules — worth a conversation, decide once
These genuinely cut both ways, which is why they generate the longest threads:
- Complexity and size limits (
complexity,max-lines,max-depth). They point at real maintainability problems, but the threshold is arbitrary and a function at 11 branches isn't meaningfully worse than one at 10. Useful as warn to spot outliers; painful as error on legacy code. no-console. Obviously right for browser code shipping to production, obviously wrong for a CLI whose entire job is printing. The answer is path-specific overrides, not a repo-wide decree.- Import ordering and naming conventions. Real value at scale — a consistent import block is easier to scan, and consistent naming makes grep work. But these only pay off if they're fully automated. If your tooling can autofix the ordering, enable it; if a human has to hand-sort imports to satisfy the rule, the cost outweighs the benefit.
no-shadow. Catches confusing reuse of a name in a nested scope, and fires on perfectly clear code often enough to annoy people.
Tier 2 rules deserve exactly one discussion, a decision recorded in the config, and no re-litigation. Which brings us to the tier that shouldn't get a discussion at all.
Tier 3: taste rules — let the formatter decide, or turn them off
Quote style, semicolons, indentation, trailing commas, line length, brace placement. These have consumed more engineering hours than any bug they've ever prevented, and they are entirely solved by running a formatter. ESLint has moved its core stylistic rules out of the recommended set, and the practical guidance follows the same direction: hand layout to Prettier, Biome, Ruff's formatter, or whatever your ecosystem's equivalent is, and delete the lint rules that cover the same ground.
The moment layout is decided by a tool nobody controls, it stops being an opinion anyone can hold. That's the actual value — not the formatting, the end of the conversation about formatting.
Severity is your negotiating tool
Most rule arguments are really arguments about consequences: what happens on a Tuesday afternoon when the rule fires on a hotfix. Severity separates "this is worth knowing" from "this cannot merge."
- Error = blocks the merge. Reserve it for Tier 1 and for settled Tier 2 decisions the team is confident in.
- Warn = visible, non-blocking. The right home for a rule on trial and for the first pass over legacy code.
- Off = the rule doesn't exist. A perfectly respectable outcome.
Warn is a staging area, not a destination. Warnings that never block anything eventually become wallpaper, and a config with 300 permanent warnings is functionally identical to a config with none. Give each trial rule a review date: promote it to error, or turn it off. Running CI with a zero-warning budget (--max-warnings 0 in ESLint's case) makes that discipline structural rather than aspirational.
Here's what a deliberate config looks like — recommended preset as the base, a small set of explicit additions, and one path-scoped exception:
// eslint.config.js
import js from "@eslint/js";
export default [
js.configs.recommended,
{
rules: {
// Tier 1: added deliberately, not in the default preset
eqeqeq: ["error", "always"],
"no-console": "error",
// Tier 2: on trial — review before the next release
complexity: ["warn", 12]
}
},
{
// CLI entry points print for a living
files: ["scripts/**/*.js", "bin/**/*.js"],
rules: {
"no-console": "off"
}
}
];
Every non-default line has a reason next to it. That comment is what stops the same argument from restarting in six months, when nobody remembers why complexity is set to 12.
Let evidence add rules, not opinions
The healthiest way to grow a config is reactive. A bug reaches production. In the postmortem, someone notices a lint rule would have caught it. That is a rule with a case attached, and nobody argues with it — it comes with an incident number.
The reverse works too. Track how often each rule gets suppressed. If one rule accounts for most of the disable comments in your repo, the rule is miscalibrated for your codebase. Fix the configuration, not the symptom. A quick count over your source tree tells you where to look:
grep -rho "eslint-disable[a-z-]*" src/ | sort | uniq -c | sort -rn
Both directions share the same principle: rules enter and leave the config because of something that happened, not because of something someone believes. Preferences produce meetings; evidence produces decisions.
One more habit that saves a lot of pain: when you enable a new rule on an existing codebase, run it before you commit to it. Turn it on locally, run the linter, and look at the actual findings. A rule that produces 4,000 hits on your repo is a migration project with a schedule, not a config line — and knowing that before the debate is far more useful than any argument about whether the rule is good in principle.
FAQ
Which ESLint rules should I enable first?
Start with js.configs.recommended and change nothing else for a week. It's curated for signal, and running it on your real codebase tells you more about your code than any rule list will. Then add eqeqeq, and on TypeScript projects the typescript-eslint recommended configuration, before considering anything else.
Is the recommended preset enough on its own?
For most teams starting out, yes. Presets are maintained to have a low false-positive rate, which is exactly the property you want while people are still forming a habit of keeping the linter at zero findings. Add rules from evidence — a bug that slipped through, a review comment made three times — rather than by reading the full rule catalog.
Should I enable stylistic lint rules if I already use a formatter?
No. Disable them. When a formatter and a linter both have opinions about layout, they either duplicate each other or conflict, and both outcomes cost you time. Layout goes to the formatter; the linter keeps rules about correctness and conventions the formatter can't express.
How do I know a lint rule is doing more harm than good?
Count its suppressions. A rule that people routinely disable inline, or that reliably produces a groan when it fires, is miscalibrated — either scope it to the paths where it's right, downgrade it to warn, or turn it off. A rule nobody respects makes every other rule easier to ignore too.
How many lint rules is too many?
There's no number — the real limit is whatever your team keeps at zero findings. A tight config that always passes clean is worth more than an exhaustive one everyone merges around. If developers routinely merge with lint findings outstanding, your config is too big regardless of how good each individual rule is.
Rule catalogs, preset quality, and how painful the config surface is to live with vary a lot between linters — and those differences matter more than any individual rule you'll argue about. Compare linters on rule coverage and configuration model on Lintense — every comparison states its criteria up front, so you can see exactly which trade-offs you're accepting before your team standardises on one.