Blog

Introduction to JavaScript Linters

Key Takeaways

  • JavaScript linting catches certain suspicious patterns and enforces project rules; it doesn't prove that your application works.
  • Use ESLint flat config for new setups. ESLint 9 made it the default, and ESLint 10 removed support for the legacy configuration format.
  • Choose ESLint for its configurable rules and plugins, typescript-eslint for TypeScript integration, or Biome when its combined linting and formatting checks cover your needs.
  • Keep configuration in the repository, get feedback in your editor, and require the same checks in continuous integration.
  • Start with useful recommended rules, review autofixes, and avoid formatting conflicts or rules your team routinely ignores.

JavaScript linting analyzes source code without running your application, looking for potential mistakes and violations of configured rules. A useful setup catches problems early and makes team conventions repeatable—not merely produces more warnings. ESLint's core concepts guide explains this distinction between potential bugs, best practices, and style checks.

What Is JavaScript Linting?

Lint rules serve two different purposes:

  • Potential-error detection: flag unused variables, references to undeclared names, and other suspicious constructs.
  • Consistency: enforce agreed conventions about how you write code, including stylistic choices when those rules are enabled.

Neither category guarantees correctness. A perfectly lint-clean function can calculate the wrong total, mishandle an API response, or accept invalid input.

The old explanation that JavaScript needs linting because it is “uncompiled” is misleading. JavaScript engines can use just-in-time compilation, and projects commonly parse, transform, bundle, and optionally type-check their source. Linting complements those checks rather than filling a complete absence of error checking. Our guide to JavaScript build tools explains the surrounding toolchain.

How JavaScript Linters Work

A linter parses source into a structured representation called an abstract syntax tree, then applies rules to that structure. ESLint also analyzes scope: where variables are declared and where they can be referenced.

In ESLint, these pieces have different jobs:

  • Core rules are built-in checks, such as no-unused-vars and no-undef.
  • Plugins add capabilities such as custom rules and framework-specific checks.
  • Parsers turn source into a structure ESLint understands. TypeScript needs an appropriate parser rather than merely a different file extension.
  • Shareable configs package reusable configuration, including rule selections and options.
  • Source-code formatters, such as Prettier, rewrite layout. Confusingly, an ESLint “formatter” instead controls how diagnostic results appear in terminal output.

Parsing support, rule behavior, and plugin compatibility are separate concerns. A parser accepting new syntax doesn't mean every framework plugin understands it—or that your deployment runtime supports it.

Choosing a JavaScript Linter

Choose based on the checks your codebase needs, not a popularity ranking. Start by listing your languages, framework requirements, custom rules, and formatting preferences.

ToolConfiguration and capabilitiesGood fitTradeoff to evaluate
ESLintJavaScript flat config, configurable rules, custom rules, and plugins.Projects needing framework-specific checks or tailored policies.You manage compatibility among ESLint, parsers, plugins, and shared configs.
ESLint with typescript-eslintTypeScript parser, rules, and presets layered onto ESLint; optional type-aware checks.TypeScript projects that want ESLint rules plus checks informed by types.Typed linting needs TypeScript project configuration and adds analysis work.
BiomeCombined linting and formatting for JavaScript, TypeScript, JSX, and TSX, with shared configuration.Projects whose required checks fit its integrated toolchain.Compare actual rule coverage and extension requirements before replacing existing tooling.

ESLint and typescript-eslint

ESLint is a strong starting point when you need configurable policies or framework plugins. typescript-eslint isn't a competing standalone linter: it supplies the TypeScript integration for ESLint.

Its optional typed linting uses TypeScript's type information for deeper checks. Enable it deliberately: resolving that information adds work beyond syntax-only linting.

Biome

Biome brings linting and formatting into one toolchain. It also supports GritQL linter plugins; don't assume those are interchangeable with ESLint plugins.

For an ESLint vs Biome decision, try representative files—including framework code—and compare the findings your team considers important. Prefer the simpler setup only after confirming that it retains the checks you need.

JSLint

Douglas Crockford's JSLint takes an opinionated approach to JavaScript conventions. It does offer configuration options and directives, so “not configurable” is too absolute; its help documentation describes those controls. Choose it when you explicitly want its conventions, rather than expecting ESLint-style customization.

JSHint

JSHint originated from JSLint and provides configurable checks. Its configuration options can be stored in a .jshintrc file, among other locations. If an existing project already gets useful feedback from it, assess migration against concrete missing checks—not an unsupported claim that every older linter is obsolete.

ESLint Setup with Flat Config

This example targets a small Node.js project using JavaScript ES modules. Use a supported Node.js release that meets ESLint's prerequisites.

1. Install the Project Dependencies

From your project directory, initialize a package only if you don't already have one, then install the development dependencies:

# Skip this command if package.json already exists.
npm init -y
npm install --save-dev eslint @eslint/js globals

2. Create the Configuration

Create eslint.config.mjs at the project root. This is a supported flat-config filename and uses ES module syntax.

// eslint.config.mjs
import js from "@eslint/js";
import { defineConfig, globalIgnores } from "eslint/config";
import globals from "globals";

export default defineConfig([
  globalIgnores(["**/dist/**", "**/coverage/**"]),
  {
    files: ["**/*.{js,mjs}"],
    extends: [js.configs.recommended],
    languageOptions: {
      globals: globals.node,
    },
    linterOptions: {
      reportUnusedDisableDirectives: "error",
    },
    rules: {
      "prefer-const": "error",
    },
  },
]);

This applies ESLint's recommended JavaScript rules and an additional prefer-const rule to matching JavaScript files. It ignores build output and coverage directories and reports unnecessary rule-disable comments as errors.

The example declares Node.js globals. For browser source, use globals.browser in the matching configuration object; mixed projects should scope each environment to its own files. ESLint's language-options guide explains globals and parser settings.

3. Add Package Scripts and Run Them

Merge this fragment into your existing package.json, preserving other scripts and dependencies:

{
  "scripts": {
    "lint": "eslint . --max-warnings 0",
    "lint:fix": "eslint . --fix"
  }
}

Run the check locally:

npm run lint

--max-warnings 0 makes warnings fail this check too. Without a warning threshold, warnings alone normally don't produce a failing exit status. The CLI reference documents these options.

Choose Useful Rules and Apply Autofix Safely

Start with recommended rules, then add rules that prevent recurring mistakes or encode conventions your team actually values. Don't select a shared config just because its style-guide name is familiar: check its maintenance, flat-config support, and declared compatibility with your installed dependencies.

Save this intentionally broken linting fixture as src/demo.mjs; it demonstrates findings, not production code:

// Deliberately broken example for linting; don't call this function.
export function totalWithTax(price) {
  let rate = 0.2;
  return price * (1 + rate) + shipping;
}
  • prefer-const reports rate because it isn't reassigned. This declaration can be automatically changed to const; see our explanation of JavaScript var, let, and const for the language distinction.
  • no-undef reports shipping because it isn't declared. You must decide whether it should be a parameter, an import, or removed.

npm run lint:fix writes available automatic fixes; it doesn't resolve every finding. Some rules instead offer editor suggestions that require a developer decision. Work from a clean commit, review the diff, and run tests afterward.

To inspect fixes without writing files, run:

npx eslint src/demo.mjs --fix-dry-run --format json

The JSON output can include transformed source. An unresolved finding can still make the command fail.

Linting vs Formatting

Linting checks rules about code; formatting standardizes its layout. You can enforce style through lint rules, use ESLint alongside Prettier, or choose an integrated tool such as Biome.

Give each concern one owner. If Prettier owns layout, disable conflicting lint rules rather than letting two tools rewrite each other's changes. Prettier's integration guidance recommends this separation and points to eslint-config-prettier, which disables conflicting rules—it doesn't run Prettier.

Integrating Linters in Your Workflow

Editors and Local Hooks

Use an editor integration, such as the VS Code ESLint extension, for feedback while editing. Keep rules in the repository and make the editor use the project's installed tooling, rather than maintaining a second personal rule set.

A pre-commit hook can provide another early check, especially on staged files. Treat it as a convenience, not the authoritative gate. Keep linting as an independent package script; a webpack or other bundler integration is optional, not a prerequisite.

Continuous Integration

Commit your configuration, package manifest, and lockfile. After checking out the repository and provisioning Node.js, a CI job can run:

npm ci
npm run lint

npm ci requires an existing lockfile and fails if it doesn't match the package manifest. Ensure the job installs development dependencies, where the lint tooling lives.

Require this non-mutating check before merging. Run tests and any applicable type checking alongside it; linting doesn't replace the behavioral checks provided by unit tests or runtime input validation.

Source files
  |-- Linter --------------------|
  |-- Tests ---------------------|--> Required CI checks --> Merge
  |-- Type checking, if used ----|

Passing these checks is not a correctness guarantee.

AI-Assisted Workflow

Use the same deterministic checks for AI-generated edits as for human-written code. Have your coding agent run the repository's lint and formatting commands after edits, address remaining findings, and report the results. Keep editor feedback and optional hooks for speed, but retain CI as the gate; don't accept rule suppression or configuration weakening merely to make generated code pass.

Common Mistakes

  • Enabling everything: start with checks you understand and value. A rule that everyone suppresses needs a team decision, not another blanket disable.
  • Adding TypeScript extensions without TypeScript configuration: use typescript-eslint's parser and presets, or your framework's documented setup.
  • Declaring every runtime global everywhere: scope browser, server, and test configuration to the files that need it.
  • Trusting editor-only success: reproduce the result with the repository's package script.
  • Debugging the wrong configuration: use npx eslint --print-config src/demo.mjs to inspect that file's calculated configuration, then check file patterns and editor settings.

FAQ

What does a JavaScript linter do?

A JavaScript linter analyzes source code for configured problems, such as unused variables, undeclared names, and convention violations. It can catch some mistakes before execution, but it doesn't guarantee runtime correctness.

How do I set up ESLint with flat config?

Install ESLint and the configuration dependencies, create an eslint.config.mjs or another supported flat-config file, and enable a recommended preset. Add a package script so developers and CI run the same check.

What is the difference between ESLint and Prettier?

ESLint checks configurable code rules, while Prettier focuses on formatting source code. You can use both, but disable overlapping formatting rules so they don't conflict.

Is ESLint or Biome better for a new JavaScript project?

Choose ESLint when you need particular plugins or custom policies. Consider Biome when its integrated linting and formatting cover your requirements; compare representative files and required rules before deciding.

Can ESLint lint TypeScript and JSX?

Yes, with appropriate configuration: typescript-eslint supplies TypeScript parsing and rules, while JSX needs matching file patterns and JSX-capable parser settings. Framework-specific checks require the relevant plugins or framework tooling; accepting syntax isn't the same as validating framework usage.

Further Reading

Written by Sam Erickson

Software engineer. I write every article on StackChief myself. Author of Mastering the Tech Interview. More about me