Formatters & Code Style

Do You Need a Code Formatter? A Practical Guide to Autoformatting

Count the minutes your team spends on code layout. The pull-request comment that just says "nit: spacing." The two developers whose editors disagree about tabs, so every file they both touch flips back and forth. The reviewer scrolling past forty lines of reindentation to find the one line that actually changed. None of it makes the software better, and all of it is a solved problem.

The short version: a code formatter rewrites your source into one canonical layout automatically, so how code looks stops being a decision anyone makes or argues about. A linter tells you your code has a problem; a formatter just fixes the layout and moves on. If your team writes code and reviews each other's code, you almost certainly want one — and this guide covers what it does, where it differs from a linter, and how to roll one out without a disruptive, blame-destroying diff.

What a formatter actually does

A formatter parses your code into a syntax tree — the same structured representation a linter builds — then throws away the original spacing and line breaks and reprints the tree using one fixed set of layout rules. Because it works from the parsed structure rather than editing text, the output is guaranteed to be equivalent code: same behavior, standardized presentation.

That reprint decides the things nobody should relitigate per file:

  • Indentation (spaces vs. tabs, and how many).
  • Where lines wrap, and how long a line may get.
  • Quote style, trailing commas, and semicolons.
  • Spacing inside brackets, around operators, and between blocks.

Run it and a chaotically-spaced file becomes indistinguishable from one a careful engineer laid out by hand — in milliseconds, with no judgment required. The defining trait of the modern generation of formatters is that they are deliberately opinionated: they expose few or no options on purpose, because every option is a debate reintroduced. Prettier is the canonical example in the JavaScript world; Go shipped gofmt with the language precisely so the community would never have a style argument at all.

Formatter vs. linter: the distinction that saves you grief

This is the single most common confusion in code-quality tooling, and getting it wrong leads to two tools fighting over the same file. The clean split:

  • A formatter decides how code looks and rewrites it to match. It has no opinion on correctness. It will happily, beautifully format a function with an obvious bug in it.
  • A linter decides whether code follows rules — probable bugs, banned patterns, unused variables, risky constructs — and reports violations, fixing some automatically.

Put differently: the formatter answers "is this laid out canonically?" and the linter answers "is this code okay?" They are complementary, and the mature setup runs both. The one rule that keeps them from colliding: turn off every stylistic rule in your linter and let the formatter own layout entirely. Historically, teams ran ESLint with dozens of formatting rules and Prettier, and the two would disagree — one wanting a line broken, the other joining it — producing an unwinnable loop. The fix the ecosystem settled on is to strip formatting rules out of the linter's job description completely, leaving it to reason about code, not columns.

If you want the wider map of how formatters, linters, type checkers, and security scanners fit together, our static analysis guide lays out the whole spectrum; this article zooms in on the formatting layer.

Why "opinionated" is a feature, not a limitation

The instinctive objection to an opinionated formatter is "but I don't like how it formats that." Sit with it for a week and the objection usually dissolves, because you were never really trading away good style — you were trading away choice, and choice about layout is a cost, not a benefit.

An opinionated formatter delivers three things a configurable one can't:

  1. The debate ends permanently. You can't argue about tab width if the tool doesn't offer the option. Every hour not spent in a style-guide meeting is the formatter paying rent.
  2. Every file looks the same. New hires, external contributors, and the intern all produce output identical to your most senior engineer's. Consistency stops depending on discipline.
  3. Diffs shrink to meaning. When layout is deterministic, a diff contains only real changes. Reviewers stop wading through reflowed whitespace, and git blame stops pointing at whoever last reformatted a block instead of whoever wrote the logic.

There is a real trade-off, stated honestly: you give up personal preference and the occasional hand-tuned alignment that read nicely. For almost every team, uniformity and zero debate are worth far more than the rare artisanal layout. That is a reason, not a decree — if your codebase genuinely depends on manual alignment (some assembly or tabular data), weigh it deliberately.

Setting up a formatter

Most formatters share the same shape: install, drop in a tiny config (or none), and wire it to run automatically. Three examples across ecosystems.

Prettier (JavaScript, TypeScript, CSS, JSON, Markdown, and more) reads an optional .prettierrc.json:

{
  "semi": true,
  "singleQuote": true,
  "trailingComma": "all",
  "printWidth": 100
}

The point of Prettier is that this file stays short — a handful of top-level knobs, not a style manual. Add a .prettierignore (same syntax as .gitignore) for anything you don't own:

dist/
build/
package-lock.json

Format the project with npx prettier --write ., or check without rewriting using npx prettier --check ..

Ruff (Python) includes a formatter compatible with the widely-adopted Black style, configured in pyproject.toml:

[tool.ruff]
line-length = 100

[tool.ruff.format]
quote-style = "double"

Run ruff format . to rewrite, or ruff format --check . to verify. Because Ruff also lints, one tool can cover both jobs in Python — a genuine convenience, though the formatter and linter remain conceptually separate steps.

gofmt (Go) ships with the toolchain and takes no configuration by design:

gofmt -w .

There is nothing to decide, which is exactly the philosophy: the Go team removed formatting from the space of things anyone configures.

The pattern generalizes — Rust has rustfmt, many languages have an equivalent — and the concepts here transfer directly; only the command changes. When you're weighing which tool to standardize on, our guide to choosing a linter and formatter for your language walks the mainstream options with the trade-off behind each.

Make it automatic or it won't stick

A formatter you have to remember to run is a formatter half your team forgets. Wire it into the workflow at two levels so formatting happens without anyone thinking about it.

Format on save, in the editor. This is where the magic is felt: you type messily, hit save, and the file snaps into canonical shape. Commit a shared editor config so new machines inherit it rather than each developer configuring it by hand. For VS Code, a checked-in .vscode/settings.json:

{
  "editor.formatOnSave": true,
  "editor.defaultFormatter": "esbenp.prettier-vscode"
}

Pair it with an .editorconfig at the repo root so even editors without the formatter plugin agree on the basics:

root = true

[*]
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true

Verify in CI, don't rewrite there. The enforcement point of record is your pipeline, and the rule is that CI checks formatting and fails if a file is unformatted — it never silently rewrites and pushes. A minimal check step:

prettier --check .

A failure here means someone's editor wasn't formatting on save; the fix is local, not a bot commit. This mirrors how lint rules get enforced across editor, hook, and pipeline — the full three-layer pattern (and why CI should verify rather than auto-fix) is covered in linting in CI.

Adopting a formatter on an existing codebase

Running a formatter on a large, never-formatted repo produces one enormous diff touching nearly every file. Done carelessly, it destroys git blame and buries real history. Done right, it's a non-event. The playbook:

  1. Reformat everything in one dedicated, code-only commit. Do the big-bang prettier --write . (or equivalent) with zero logic changes mixed in. One mechanical commit is honest; formatting sprinkled through feature PRs forever is not.
  2. Tell git blame to skip it. Record that commit's hash in a .git-blame-ignore-revs file. Git and GitHub's blame view then jump straight past the mechanical reformat to the engineer who actually wrote each line, so history stays readable.
  3. Turn on format-on-save and the CI check the same day. Once the tree is uniformly formatted, the automation keeps it that way with near-zero ongoing diff. Adopt the enforcement in the same change so the codebase never drifts back.

After that first commit, formatting stops being a topic. New code arrives pre-formatted from the editor, CI guards the door, and reviews contain only substance.

FAQ

Do I still need a linter if I have a formatter?

Yes — they catch entirely different things. A formatter standardizes layout but has no opinion on correctness; it will format a function with an unused variable, a == type-coercion bug, or an unhandled promise just as neatly as a correct one. The linter is what flags those. Run both, and disable your linter's stylistic rules so the formatter owns layout uncontested.

Won't reformatting the whole repo ruin git blame?

Only if you don't plan for it. Put the reformat in its own commit with no logic changes, then add that commit's hash to a .git-blame-ignore-revs file. Git blame and GitHub's blame view respect that file and skip the mechanical commit, pointing at the real author of each line instead of the reformat.

Should CI auto-format code and push a commit back?

Most teams shouldn't. Bot commits to pull-request branches complicate signatures, re-trigger pipelines, and quietly teach people to outsource hygiene to the robot. Keep rewriting local — format-on-save plus a pre-commit hook — and let CI only run --check and fail. The developer whose commit failed simply formats locally and pushes again.

How do I stop ESLint and Prettier from fighting?

Disable the formatting-related rules in the linter so it stops having opinions about layout, leaving those to the formatter. In practice that means removing stylistic rules from your linter config and letting Prettier (or your formatter) own everything about presentation. The linter then focuses on what it's good at — probable bugs and banned patterns — and the two never disagree.

Which formatter should my team use?

Use your ecosystem's mainstream, well-maintained option: Prettier for JavaScript/TypeScript and web assets, Ruff's formatter (Black-compatible) or Black for Python, gofmt for Go, rustfmt for Rust. The specific choice matters less than picking a widely-adopted, opinionated one and running it automatically everywhere. Judge candidates on language support, speed, and how little you have to configure.


A formatter is the cheapest consistency your codebase will ever buy: install it, let it be opinionated, run it automatically in the editor and verify it in CI, and an entire category of pull-request friction disappears. Layout stops being a decision, and your reviews get their attention back for the code that matters. Compare the leading formatters and linters for your language on Lintense — every comparison names its criteria, so you can see exactly why one fits your stack before you standardize on it.

Comments are disabled for this article.