DEV Community

Cover image for How ESLint Actually Works: The Quality Gate Behind Modern JavaScript
Utkarsh Bansal
Utkarsh Bansal

Posted on

How ESLint Actually Works: The Quality Gate Behind Modern JavaScript

A few days ago, I shared an article:

You Don't Need Another Agent. You Need a Linter.

Then I did what I do with anything I write: shared it around — a few publications, a few channels.

Two reasons:

First, feedback. I'd genuinely rather get roasted and fix my blind spots than stay comfortable and wrong.

Second, let's be honest: reach.
Every writer enjoys seeing a few more views.

Most of the responses were positive.

One wasn't.

A publication rejected it with the reason:

LOW_QUALITY

Fair enough.

It means there's room for improvement.

Funny enough, my caffeinated 1 AM brain disagreed.

Then it did what every developer does when someone says "this isn't good enough."

It took that personally.

So I went back and reread the article.

And after the initial ego check, I realized something serious:

The article talked in detail about ESLint, why it matters more in an AI-assisted world than ever.

What it did not do was answer the question that actually matters:

What is ESLint, how does it work, and why has half the JavaScript ecosystem quietly built its quality process around it?

So let's fix that.

Now, this isn't a sequel to my last piece about untangling vibe-coded code. It stands on its own — one thing, done properly.

A complete teardown of ESLint:

  • What it is
  • How it works internally
  • Why companies use it as a quality gate
  • The different classes of problems it solves
  • How plugins work
  • How to write your own rules
  • Where it fails
  • Why it still beats many AI-based review systems

Fair warning.

This article is going to be technical.

There will be syntax trees.

There will be compiler concepts.

There will be enough JavaScript internals to make frontend developers slightly uncomfortable.

I'll try my best to keep it readable not letting it turn into another manual - which nobody finishes.

Let's start with the question most people never ask.

What Is ESLint Actually Doing?

Most developers describe ESLint like this:

It checks code for mistakes.

Technically true. Also completely useless.

That's like describing a car as:

It helps you move.

The interesting part is how?

Consider this code:

import axios from "axios";

function getUsers() {
  return fetch("/api/users");
}
Enter fullscreen mode Exit fullscreen mode

With the right rules on, ESLint can tell us:

  • axios is imported and never used
  • fetch is being used
  • getUsers exists
  • getUsers is never called
  • there are no syntax errors

But how?

The answer is that ESLint never reads code as text.

The first thing it does is destroy your beautiful source code.

Step 1: Your Code Stops Being Code

The moment ESLint sees a file, it passes it through a parser.

The parser converts your code into something called an Abstract Syntax Tree (AST).

Your code:

const answer = 42;
Enter fullscreen mode Exit fullscreen mode

becomes something closer to:

{
  "type": "VariableDeclaration",
  "kind": "const",
  "declarations": [
    {
      "id": { "name": "answer" },
      "init": { "value": 42 }
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Notice what's missing.

Formatting. Spacing. Comments. Indentation.

The parser doesn't care. The AST only cares about meaning.

To ESLint, your source code isn't text anymore.

It's a tree.

Literally. That same const answer = 42 looks like this:

VariableDeclaration · const
└─ VariableDeclarator
   ├─ Identifier · "answer"
   └─ Literal · 42
Enter fullscreen mode Exit fullscreen mode

The JSON above and this tree are the same thing. One's just easier to look at.

And every lint rule is basically a tree inspector.

That's the core idea:

Source code
   ↓
Parser
   ↓
AST
   ↓
Rules inspect nodes
   ↓
Reports / warnings / errors
Enter fullscreen mode Exit fullscreen mode

Step 2: Rules Walk The Tree

Every ESLint rule is a visitor.

It walks through the nodes inside the AST and asks questions.

For example:

console.log("debug");
Enter fullscreen mode Exit fullscreen mode

produces a node that looks like:

CallExpression
Enter fullscreen mode Exit fullscreen mode

The no-console rule simply visits every CallExpression and asks:

Is this a console call?

If yes.

Report error.

That's it.

No AI. No embeddings. No vector database.

Just deterministic tree traversal.

Computer science doing computer science things.

Why This Is More Powerful Than It Sounds

Most people hear "static analysis" and immediately think:

So... it catches semicolons?

No.

Semicolons are the least interesting thing ESLint does.

Let's look at the categories.

Category 1: Correctness

These rules catch code that is probably wrong.

A function that doesn't exist:

foo();
Enter fullscreen mode Exit fullscreen mode

An assignment hiding inside a condition:

if (user = admin) { /* ... */ }
Enter fullscreen mode Exit fullscreen mode

A promise nobody awaited:

promiseFunction();
Enter fullscreen mode Exit fullscreen mode

These are not style issues. These are bugs.

The code may compile. It may even pass tests.

But eventually somebody is getting paged at 2 AM.

Category 2: Dead Code

AI loves dead code. Humans create it too. AI just does it faster.

An import that's never used:

import lodash from "lodash";
Enter fullscreen mode Exit fullscreen mode

A function nobody calls anymore:

function oldImplementation() {}
Enter fullscreen mode Exit fullscreen mode

One abandoned experiment later, and your repository becomes an archaeological site.

Dead code increases bundle size, maintenance cost, and confusion.

The cheapest bug is the code that doesn't exist.

ESLint helps make that happen.

Category 3: Security

This is where people start underestimating linters.

Consider:

eval(userInput);
Enter fullscreen mode Exit fullscreen mode

No. Just no.

Or:

element.innerHTML = userInput;
Enter fullscreen mode Exit fullscreen mode

Also no.

A decent security-focused lint setup can stop entire classes of vulnerabilities before the code ever reaches production.

Not because it's intelligent.

Because it recognizes dangerous patterns.

And dangerous patterns repeat. A lot.

Category 4: Performance

Some rules exist purely because developers keep making the same expensive mistakes.

This:

array.indexOf(value) !== -1;
Enter fullscreen mode Exit fullscreen mode

can become this:

array.includes(value);
Enter fullscreen mode Exit fullscreen mode

Tiny improvement.

Tiny improvement.

Tiny improvement.

Now repeat it across a million lines of code, and you've quietly compressed years of engineering experience into a config file.

That's what lint rules really are.

Institutional memory

The Real Superpower: Quality Gates

The biggest mistake teams make is treating ESLint like a suggestion.

A linter running in your editor is nice.

A linter blocking merges is transformative.

The moment ESLint becomes part of CI, it stops being advice.

It becomes policy.

The workflow changes from:

Hopefully somebody notices the bug in review.

to:

The code physically cannot merge.

That's a completely different game.

Good engineering isn't about creating perfect developers.

It's about creating systems where common mistakes cannot survive.

ESLint is one of those systems.

Developer writes code
   ↓
Editor lint shows warnings
   ↓
PR created
   ↓
CI runs lint
   ↓
Pass → merge
Fail → fix before merge
Enter fullscreen mode Exit fullscreen mode

Community Plugins Already Solved Half Your Problems

Here's the part most developers miss.

ESLint is a rule engine.

It parses code, walks the AST, runs rules, reports violations, and that's basically the entire core.

The real power comes from everything built on top of it: plugins, shared rule sets, and years of accumulated engineering knowledge.

Need to find unused imports?

There's a plugin. eslint-plugin-unused-imports

Need TypeScript-aware analysis?

There's a plugin. @typescript-eslint

Need accessibility checks for your components?

There's a plugin. eslint-plugin-jsx-a11y or eslint-plugin-vuejs-accessibility

Need to catch dangerous security patterns?

There's a plugin. eslint-plugin-security

Need to enforce architectural boundaries between layers?

Believe it or not, there's a plugin for that too. eslint-plugin-boundaries.

You don't have to write any of this yourself.

Thousands of developers have already spent years turning common mistakes into reusable rules.

A few common examples:

  • eslint-plugin-unused-imports — dead imports and variables
  • eslint-plugin-import — circular dependencies and broken paths
  • @typescript-eslint — type-aware correctness rules
  • eslint-plugin-jsx-a11y / eslint-plugin-vuejs-accessibility — accessibility
  • eslint-plugin-promise — dropped awaits and async mistakes
  • eslint-plugin-security — dangerous patterns like eval
  • eslint-plugin-boundaries — architectural layer rules

That's the mental shift.

Most people think ESLint is one tool with a bunch of built-in rules.

It's closer to a platform.

A small, stable engine that lets an entire ecosystem contribute knowledge.

  eslint-plugin-import     @typescript-eslint     your local rules
   (cycles, dead code)     (type-aware checks)     (team gates)
            │                      │                      │
            └──────────────────────┼──────────────────────┘
                                   ▼
            ┌──────────────────────────────────────────┐
            │                ESLint core                 │
            │     parse → walk the AST → run → report     │
            └──────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

That's why ESLint has survived for so long.

The core doesn't need to know about React.

Or Vue.

Or TypeScript.

Or whatever framework we'll all be rewriting our applications in next year.

The engine stays the same.

The ecosystem keeps growing.

The core never changes. The rules feeding into it are infinite.

And eventually you realize something interesting:

Most teams don't need more tooling.

They just need to enable the plugins that already exist.

When The Defaults Aren't Enough: Write Your Own Rule

Now for the fun part — the part that makes ESLint feel less like a tool and more like a system.

Suppose your company has a design system. And somebody keeps committing raw colors:

color: "#FF0000";
Enter fullscreen mode Exit fullscreen mode

instead of:

color: tokens.error;
Enter fullscreen mode Exit fullscreen mode

You can complain. You can review every PR. Or you can automate the complaint.

Here's the entire rule:

// rules/no-raw-color.js
export default {
  meta: {
    type: "problem",
    messages: { rawColor: "Use a design token, not a raw hex like '{{value}}'." },
  },
  create(context) {
    return {
      // the walker hands you every string literal; you keep the ones that look like hex
      Literal(node) {
        if (typeof node.value === "string" && /^#[0-9a-f]{3,6}$/i.test(node.value)) {
          context.report({ node, messageId: "rawColor", data: { value: node.value } });
        }
      },
    };
  },
};
Enter fullscreen mode Exit fullscreen mode

That's it. No npm package, no publishing. Flat config lets you wire a local rule in directly, and ESLint's built-in RuleTester lets you prove it fires on "#FF0000" and stays quiet on tokens.error.

A custom ESLint rule is usually less code than the Slack argument it prevents.

That's the real secret.

The best engineering teams don't repeatedly solve the same problem.

They automate it.

Every bug becomes a rule. Every rule becomes institutional knowledge. Every future developer benefits.

Human. Or AI. Doesn't matter.

The gate treats everybody equally.

Where ESLint Fails

Now, before somebody accuses me of turning ESLint into a religion — it has limits.

ESLint cannot tell:

  • if your architecture is good
  • if your business logic is correct
  • if the feature should even exist
  • if your product manager had a terrible idea

Those remain human problems.

Static analysis sees structure. Not intent.

A green lint report does not mean correct software.

It means your code survived a specific set of inspections.

Nothing more. Nothing less.

So Why Am I Still Talking About ESLint In The Age Of AI?

Because every week I see somebody proposing:

  • AI review agents
  • AI architecture agents
  • AI quality agents
  • AI code verification agents

And half the time, they're trying to solve problems ESLint solved years ago.

Before building another agent, ask:

Can a deterministic static analysis tool already solve this?

Because if the answer is yes, the boring tool is:

  • faster
  • cheaper
  • reproducible
  • and it doesn't hallucinate

The boring solution often wins.

Not because it's exciting. Because it works.

And software engineering eventually rewards whatever keeps production alive — even if it was invented twenty years ago.

Takeaway

The most important thing I learned from AI-assisted development wasn't how powerful AI is.

It was how valuable boring tooling remains.

ESLint isn't glamorous. Nobody posts screenshots of lint checks on LinkedIn. Nobody raises venture capital for a missing-semicolon detector.

But every large JavaScript codebase eventually discovers the same truth.

It parses code. It walks trees. It applies rules. It reports violations. That sounds humble — but the impact compounds: it catches bugs early, enforces standards, protects architecture, scales institutional memory, and keeps bad code from sneaking through the door.

And unlike the rest of us, it never gets tired.

The cheapest reviewer on your team is still a linter.

If you still think of ESLint as a semicolon cop, you're missing most of the point.

It was never about the semicolons.

Top comments (0)