View all services
Talk to QA Advisor
Browse the Knowledge Hub211 resources
Tools Library · Review and guide

ESLint review: what it is, pros, cons and setup

ESLint is an open-source, pluggable linter for JavaScript, with TypeScript support through typescript-eslint and official plugins for JSON, Markdown and CSS. Rules find bugs and enforce conventions, and many fix what they find. For QA teams its value is the test-framework plugins, which catch tests that assert nothing, missing awaits and fixed waits. It is MIT licensed and hosted by the OpenJS Foundation.

QAble has evaluated this tool and works alongside it. The factual sections are complete; our view is bounded to what we have actually seen rather than a long-term verdict.

last reviewed
13 October 2026
version covered
ESLint 10
licence
Open source

Reviewed by Viral Patel

Co-Founder, QAbleLast reviewed 13 October 2026

At a glance

ESLint at a glance

Official site
Vendor
OpenJS Foundation
Category
Code quality
Licence
Open source (MIT)
Platforms
Node.js
Languages
JavaScript, TypeScript
Integrates with
Prettier, GitHub Actions, VS Code
First released
2013
Version reviewed
ESLint 10

Verdict

Our verdict on ESLint

Strengths

  • Test plugins catch assertion-free tests, focused tests, fixed waits and unawaited Playwright calls before they run
  • Type-aware no-floating-promises finds missing awaits that cause intermittent failures in async test code
  • Many rules autofix with --fix, and suggestions offer manual fixes through the editor
  • Bulk suppressions let a strict rule apply to new code while existing violations are fixed gradually
  • Free under MIT, governed by the OpenJS Foundation, with an enormous plugin ecosystem

Weaknesses

  • Version 10 removed .eslintrc, so older guides and some plugin examples no longer work
  • Type-aware rules are slower and need a tsconfig the linter can find
  • Configuration spreads across many plugins whose config names and versions must line up
  • Warnings do not fail the build by default, so they accumulate unless capped

Our take

For QA work, ESLint matters less as a style tool than as a cheap defect finder for test code. The Playwright, Cypress and Jest plugins flag tests with no assertions, focused tests left in, fixed sleeps and unawaited calls, and typescript-eslint’s type-aware no-floating-promises rule catches the missing await that causes most intermittent failures in async test code. Lint test code to the same standard as production code and much flakiness never reaches CI. QAble has evaluated ESLint for this purpose rather than maintained a long-running configuration of its own, so treat this as a bounded take. Version 10 removed the old .eslintrc format entirely, so many plugin READMEs and blog posts show configuration that no longer loads. Use flat config examples only, and expect to adapt older guides.

We have evaluated this tool rather than maintained a long-running suite on it, so this is a bounded view rather than a long-term verdict. The rest of this page is factual.

How it works

What is ESLint and how does it work?

Parse, walk the tree, report

ESLint parses each file into an abstract syntax tree, using its own parser for JavaScript or a plugin parser such as typescript-eslint’s for TypeScript. It then walks the tree, and every enabled rule receives the node types it subscribed to, such as call expressions or functions, and reports problems with a location and, where it is safe, a fix or a suggestion. Rules work on one file at a time and know nothing about other files unless a parser supplies type information or a plugin resolves imports. That design is why plain ESLint is fast and predictable, and why the checks that need types, such as unawaited promises, require the slower typed setup.

Flat config: an ordered array of objects

Configuration lives in eslint.config.js, or .mjs, .cjs or TypeScript variants, which exports an array of configuration objects. Each object can name the files it applies to, files to ignore, language options such as globals and parser, plugins, and rules. Objects later in the array override earlier ones for the files they match, and defineConfig adds an extends key for building on shared configs. Nothing is linted until you enable rules or extend a shared config. In version 10, ESLint looks for the configuration file starting from the directory of each file being linted and searches upwards, so a monorepo package can carry its own config.

Type-aware rules borrow the TypeScript compiler

Some of the most valuable rules for test code need type information: no-floating-promises must know that a call returns a promise before it can complain that nothing awaits it. typescript-eslint provides these through its TypeChecked shared configs, with parserOptions.projectService set to true so the TypeScript language service supplies types for each file. The cost is speed, since the compiler now runs inside the linter, and a subtle interaction with caching: the ESLint documentation warns that cached results do not track dependencies between files, so type-aware rules can report stale results when another file changes.

Severities, exit codes and suppressions

Each rule is off, warn or error. Errors make ESLint exit with code 1; warnings do not affect the exit code unless --max-warnings sets a threshold, which is why warning counts tend to climb unnoticed. Inline comments can disable a rule for a line or file. For adopting a rule across an existing codebase, bulk suppressions record current violations of error-level rules in an eslint-suppressions.json file, so only new violations fail. When a suppressed violation is fixed, ESLint reports the unused suppression and exits non-zero until it is pruned, which keeps the list honest.

Strengths

What it does well

Rules written for test code

eslint-plugin-playwright’s recommended config includes missing-playwright-await, no-wait-for-timeout, expect-expect, no-focused-test, no-skipped-test, no-networkidle and prefer-web-first-assertions. eslint-plugin-cypress recommends no-unnecessary-waiting, no-assigning-return-values, no-async-tests and unsafe-to-chain-command. eslint-plugin-jest flags focused and disabled tests and invalid expect calls. These encode each framework’s best-practice guide as checks that run before a test does.

Fixes, not just findings

Rules marked fixable apply their corrections with --fix, and --fix-type limits that to problem, suggestion, layout or directive fixes. Other rules offer suggestions that editors present as quick fixes rather than applying automatically, because the change needs a human decision. Adding a missing await in a Playwright test is a one-keystroke fix in the editor.

Adopting strict rules on legacy code

Bulk suppressions solve the old problem of enabling a rule on a large codebase. Turn the rule on as an error, run eslint --fix --suppress-all or --suppress-rule for one rule, and commit the suppressions file. New violations fail immediately; existing ones are fixed at the team’s pace, and --prune-suppressions removes entries as they are resolved.

Speed on large repositories

--cache lints only changed files, with a content-based strategy available for CI checkouts where file times are unreliable. --concurrency runs linting across worker threads, with auto choosing the number. Together they keep lint times reasonable on large monorepos, though caching should be used carefully with type-aware rules.

More than JavaScript

ESLint’s language plugins extend it beyond JavaScript: @eslint/json for JSON and JSONC, @eslint/markdown for Markdown and code blocks in documentation, and @eslint/css for stylesheets. A single configuration and a single CI step can cover test data files and documentation alongside the code.

Editor and agent integration

Editor extensions show problems as you type and apply fixes on save, which is where most lint value is captured. ESLint also ships an MCP server, started with eslint --mcp, so AI coding agents can lint the code they write against the project’s own configuration rather than generic conventions.

Pricing

ESLint pricing and licensing

ESLint is free and open source under the MIT licence, with no paid edition, hosted service or licence key. It is an OpenJS Foundation project funded by sponsors and individual backers through Open Collective. The plugins named on this page are also free open-source packages. The costs are the ones every linter carries: CI minutes, especially for type-aware rules, and engineering time to choose rules, triage findings and upgrade across major versions.

  • Licence: MIT for ESLint itself, copyright the OpenJS Foundation and contributors.
  • No tiers: every rule, formatter, cache, suppression and concurrency option is in the open-source package.
  • Plugins: eslint-plugin-playwright, eslint-plugin-cypress, eslint-plugin-jest and typescript-eslint are free, maintained by their own communities on their own release schedules.
  • CI cost: type-aware linting runs the TypeScript compiler, so it takes noticeably longer than syntax-only rules on large repositories.
  • Hidden cost: major upgrades. Version 10 removed the old config format and raised the Node.js floor, so plan migration time rather than bumping the version in passing.

Checked against the vendor’s own pages on 13 October 2026. Pricing changes; confirm on the official site before buying.

Getting started

Getting started with ESLint

Setup, step by step

  1. Check Node.js: ESLint 10 needs 20.19 or later, 22.13 or later, or 24 and above.
  2. Run npm init @eslint/config@latest in a project that already has package.json, and answer the prompts.
  3. Open the generated eslint.config.mjs and add the plugin for your test framework, scoped to test files.
  4. Run npx eslint . and fix or discuss the first findings.
  5. Add a lint script and run it in CI with --max-warnings 0.

Install

npm init @eslint/config@latest
# or manually
npm install --save-dev eslint @eslint/js eslint-plugin-playwright
npx eslint .

A first example

// eslint.config.mjs
import { defineConfig } from 'eslint/config'
import js from '@eslint/js'
import globals from 'globals'
import playwright from 'eslint-plugin-playwright'

export default defineConfig([
  { ignores: ['playwright-report/**', 'test-results/**'] },
  {
    files: ['**/*.js'],
    plugins: { js },
    extends: ['js/recommended'],
    languageOptions: { globals: globals.node },
  },
  {
    // Test rules only where tests live
    files: ['tests/**'],
    extends: [playwright.configs['flat/recommended']],
    rules: {
      // Fail, not warn: a focused test silently skips the rest of the suite
      'playwright/no-focused-test': 'error',
    },
  },
])

What trips people up first

Configuration copied from an older guide. ESLint 10 no longer supports .eslintrc files at all, so a project with only .eslintrc.json either finds no configuration or ignores it, and plugin READMEs that still show a legacy block next to the flat one invite the wrong copy. Use eslint.config.mjs and each plugin’s flat config export, such as playwright.configs['flat/recommended'] or pluginCypress.configs.recommended. The other early surprise is that a /* eslint-env */ comment in an old file is now reported as an error; delete it and declare globals through languageOptions.globals instead.

In practice

Using ESLint in practice

Separate blocks for application and test code

Keep one config file and scope each block with files globs: application rules for src, test-framework rules for the test folders, and a block for configuration files that run in Node. Test plugins should never apply to application code, where rules such as expect-expect make no sense. Give test files the globals their runner expects through languageOptions.globals or the plugin’s own config, rather than turning off no-undef. Put generated folders such as reports, coverage and build output in a global ignores block at the top. A useful habit is to promote the rules that indicate real defects to error, such as focused tests and fixed waits, and leave stylistic preferences at warn or off so the lint output stays meaningful.

Catching missing awaits with typed linting

The most expensive bug in async test code is an action that is not awaited: the test moves on, fails somewhere else, or passes when it should not. eslint-plugin-playwright’s missing-playwright-await covers Playwright’s own APIs, and typescript-eslint’s no-floating-promises covers every promise, including your page object methods. The latter needs type information, which recommendedTypeChecked and projectService provide. Apply typed rules to TypeScript files only, make sure every linted file is covered by a tsconfig, and expect lint time to rise noticeably on large suites.

// eslint.config.mjs (TypeScript test project)
import { defineConfig } from 'eslint/config'
import js from '@eslint/js'
import tseslint from 'typescript-eslint'
import playwright from 'eslint-plugin-playwright'

export default defineConfig([
  { ignores: ['playwright-report/**', 'test-results/**'] },
  {
    files: ['**/*.ts'],
    extends: [js.configs.recommended, tseslint.configs.recommendedTypeChecked],
    languageOptions: {
      // Ask the TypeScript language service for each file's types
      parserOptions: { projectService: true },
    },
    rules: {
      // Catches await-less calls to your own page objects too
      '@typescript-eslint/no-floating-promises': 'error',
    },
  },
  {
    files: ['tests/**/*.ts'],
    extends: [playwright.configs['flat/recommended']],
  },
])

// Flagged by both rules:
// test('saves', async ({ page }) => {
//   loginPage.signIn('[email protected]')   // floating promise
//   page.getByRole('button', { name: 'Save' }).click()   // missing await
// })

Running ESLint in GitHub Actions

Lint is the cheapest check in a pipeline, so run it first and fail fast. Pass --max-warnings 0 if warnings are meant to be acted on, otherwise they are just noise. --concurrency auto spreads files across threads on multi-core runners. Be careful with --cache in CI when type-aware rules are enabled, since cached results do not track cross-file changes; a full typed run on pull requests and a cached syntax-only run locally is a reasonable split. The built-in json formatter, selected with --format json, turns results into something other tools can read and annotate.

# .github/workflows/lint.yml
name: lint
on: [push, pull_request]
jobs:
  eslint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-node@v7
        with:
          node-version: 24
          cache: npm
      - run: npm ci
      # Any warning fails the job; threads chosen automatically
      - run: npx eslint . --max-warnings 0 --concurrency auto

Tightening rules on an existing suite

Turning a rule such as no-floating-promises on across a large existing test suite usually produces hundreds of findings, and the classic outcome is that the rule is switched back to warn and forgotten. Bulk suppressions avoid that. Enable the rule as an error, run eslint --fix --suppress-rule with the rule name so auto-fixable cases are fixed rather than suppressed, and commit eslint-suppressions.json. From then on, new code must comply, while the suppressions file is a visible backlog that shrinks with every fix. Run --prune-suppressions after cleanup sessions. Only error-level rules can be suppressed this way; warnings are not recorded.

Upgrading to ESLint 10

Version 10.0.0 was released on 6 February 2026 and the 10.x line has continued with minor releases since. The upgrade is mostly configuration work. ESLint publishes codemods: npx codemod @eslint/v9-to-v10 removes legacy flags and environment variables and updates custom rules and API usage. Check Node.js first, including the version your editor’s ESLint extension uses. Move any remaining .eslintrc configuration to flat config, since the old format and the ESLINT_USE_FLAT_CONFIG switch are gone. Expect new findings: eslint:recommended now enables no-unassigned-vars, no-useless-assignment and preserve-caught-error, and JSX references are now tracked, which changes no-unused-vars results in component tests. Check each plugin’s supported ESLint range; eslint-plugin-cypress 7, for example, requires ESLint 10.

Pitfalls

Common pitfalls

An .eslintrc file that ESLint 10 ignores

Why it happens: Version 10 removed the legacy configuration format and the ESLINT_USE_FLAT_CONFIG escape hatch, along with the FlatESLint and LegacyESLint APIs.

The fix: Migrate to eslint.config.mjs using the configuration migration guide or the v9-to-v10 codemod, and use each plugin’s flat config export rather than plugin: strings.

eslint-env comments reported as errors

Why it happens: Older files declare globals with /* eslint-env */ comments, which flat config never supported and which version 10 now reports as errors during linting.

The fix: Delete the comments and declare globals in languageOptions.globals for the matching files, for example globals.node for config scripts or the test framework’s globals for specs.

Test rules applied to the whole repository

Why it happens: Extending a test plugin config at the top level applies rules like expect-expect to application code and config files, producing nonsense findings that get the plugin removed.

The fix: Scope every test plugin block with files globs that match only test folders or file suffixes, as the Playwright plugin’s own README recommends.

Stale results from --cache with typed rules

Why it happens: The cache decides validity from the file and the config only. Type-aware rules depend on other files, so a change in a shared type can leave cached results wrong.

The fix: Avoid --cache for runs that include type-aware rules, or clear the cache when shared types change, and keep the authoritative typed run uncached in CI.

Warnings that never fail anything

Why it happens: Warn-level rules do not affect the exit code, so findings accumulate until the output is too long for anyone to read.

The fix: Decide per rule whether it matters. Promote the ones that do to error, switch the rest off, and run CI with --max-warnings 0 or a fixed threshold.

Plugin and ESLint versions out of step

Why it happens: Plugins declare supported ESLint ranges. eslint-plugin-cypress 7 requires ESLint 10, and older Cypress plugin versions exposed a /flat config that has since been removed.

The fix: Upgrade ESLint and its plugins together, read each plugin’s changelog for renamed configs, and let npm’s peer dependency warnings fail the install in CI.

Compared

ESLint alternatives compared

ToolLanguages lintedFormatterExtending rulesRuns asLicence
ESLintJavaScript; TypeScript, JSON, Markdown, CSS via pluginsNo; linting onlynpm plugins with JavaScript rulesNode.js CLI, editor, CIMIT
BiomeJS, TS, JSON, CSS, GraphQL and moreYes, built inGritQL pluginsStandalone CLI via npm, editor, CIMIT or Apache 2.0
SonarQubeOver 20 languages free, more in paid editionsNoQuality profiles choose built-in rulesServer or cloud, fed by CI scannerLGPL Community Build; commercial editions

FAQ

Frequently asked questions

What is ESLint?

ESLint is an open-source, pluggable linter for JavaScript, with TypeScript support through typescript-eslint and official plugins for JSON, Markdown and CSS. Rules find bugs and enforce conventions, and many fix what they find. For QA teams its value is the test-framework plugins, which catch tests that assert nothing, missing awaits and fixed waits. It is MIT licensed and hosted by the OpenJS Foundation.

What are the best ESLint alternatives?

The alternatives most often weighed against ESLint are Biome and SonarQube. The comparison on this page sets them side by side on languages linted, formatter, extending rules, runs as and licence, and the verdict above says where ESLint is the wrong choice and which of them to pick instead.

Is ESLint free?

Yes. ESLint is open source under the MIT licence with no paid edition or hosted service, and it is an OpenJS Foundation project funded by sponsors and backers. The common plugins for testing, including eslint-plugin-playwright, eslint-plugin-cypress, eslint-plugin-jest and typescript-eslint, are free as well. The only costs are CI time and the effort of choosing rules and keeping up with major versions.

What changed in ESLint 10?

Released on 6 February 2026, ESLint 10 removed the old .eslintrc configuration format, the ESLINT_USE_FLAT_CONFIG switch and the FlatESLint and LegacyESLint APIs. It requires Node.js 20.19, 22.13 or 24 and later, reports /* eslint-env */ comments as errors, finds config files relative to each linted file, tracks JSX references, and adds three rules to eslint:recommended. Codemods automate much of the migration.

How do I lint Playwright tests with ESLint?

Install eslint-plugin-playwright and add a block to eslint.config.mjs whose files glob matches only your tests, extending playwright.configs['flat/recommended']. That enables rules such as missing-playwright-await, no-wait-for-timeout, expect-expect, no-focused-test and prefer-web-first-assertions. For TypeScript suites, add typescript-eslint’s type-aware no-floating-promises as well, which catches unawaited calls to your own helpers and page objects.

Can ESLint catch a missing await in tests?

Yes, in two ways. Framework plugins know their own APIs: eslint-plugin-playwright’s missing-playwright-await flags unawaited Playwright calls, and eslint-plugin-cypress’s no-async-tests flags async Cypress tests, where await is a mistake. For any promise, including your page objects, typescript-eslint’s no-floating-promises rule uses type information to report promises that are neither awaited nor handled.

Should I use ESLint or Biome?

Biome combines a formatter and a linter in one standalone tool with a large built-in rule set, and is attractive for a new project that wants one fast tool and few dependencies. ESLint’s advantage for QA teams is its plugin ecosystem: the Playwright, Cypress and Jest plugins and typescript-eslint’s type-aware rules have no direct Biome equivalents, since Biome extends through GritQL plugins rather than ESLint plugins.

Do I need SonarQube if I already run ESLint?

They overlap less than it seems. ESLint runs in the editor and on every commit with rules you choose, including test-framework rules SonarQube does not have. SonarQube adds a server or cloud dashboard, coverage and duplication tracking, quality gates on new code, security analysis and support for many languages beyond JavaScript. Many teams run both, with ESLint as the fast first check.

Alternatives

ESLint alternatives worth weighing

Resources

Where to go next

Ready

Picking the wrong tool costs more than picking late.

QAble advises on toolchain decisions and then implements them, with engineers who have run these tools on real delivery. Start with a free QA audit.

Talk to QA Advisor