The Job Nobody Respects: What Lint Actually Catches

Part two of the pipeline series — last time was the overview of the whole chain. This time, the job that runs first and gets the least respect: lint.
"It's just style, right?"
The first time I got a PR blocked by a lint failure, I remember being annoyed. Missing semicolon, inconsistent quote style, new line at the end of file — cosmetic stuff, surely, and definitely not worth failing a build over. It took working on a codebase with hundreds of contributors to understand why that reaction was wrong. Lint isn't just style. A good linter catches real bugs: a variable you assigned but never used (usually a sign you meant to use a different one), an == where you meant === and just introduced a type-coercion bug, an unreachable branch after a return, a .then() with no .catch() on a promise nobody's watching. None of that is about tabs versus spaces.
Two different jobs wearing one hat
"Lint" usually bundles two genuinely different things:
- Linting (ESLint, Pylint, golangci-lint) — rule-based static checks for patterns that are usually bugs or bad practice. Configurable, and the configuration is a real decision: which rules are errors, which are warnings, which are off entirely.
- Formatting (Prettier, Black, gofmt) — deterministic code layout. Not "is this good," just "is this the one agreed-upon shape." Its entire value is ending an argument nobody should be having in a code review.
Keeping them separate matters. A formatter should never need configuration debates — that defeats the point. A linter should.
Easiest to see by running both on the same messy snippet. Try the linter, then the formatter, and notice which findings survive:
Nothing has run yet. Run either tool and see what it finds.
Why it runs first
Pipelines are usually ordered cheapest-and-fastest first, most-expensive last, so a build fails as early as possible and doesn't burn compute (or a queue slot on a shared runner) on a build or test suite that was never going to matter because the diff had an obvious bug in it. Lint takes seconds. A full test suite might take minutes. Running lint after tests would mean waiting five minutes to find out about a typo you could've known about in five seconds.
Local hook vs CI job — why you need both
Most teams run lint twice: once as a local pre-commit hook (fast feedback, before the code even leaves your machine) and again in CI. That's not redundant — a local hook can be skipped (git commit --no-verify), a laptop's linter version can drift from what CI enforces, and CI is the only copy of the check nobody can bypass by accident. The local hook is a courtesy. The CI job is the actual gate.
One setting worth knowing about: many CI configs set something like --max-warnings=0, turning every warning into a hard failure. Your editor might just underline a warning in yellow and let you keep going. CI, deliberately, won't.
Next in the series: type checking — a different class of bug than lint catches, and why most pipelines run it as its own separate job instead of folding it into either lint or tests.
Enjoyed this one? Get the next post by email — no spam, unsubscribe anytime.
Subscribe →