Learning path · Level 6 · Tutorial 5

Check JavaScript with ESLint

Configure ESLint for browser JavaScript, run checks through npm, and resolve useful diagnostics without mistaking lint success for complete correctness.

  • Level: Intermediate
  • Duration: 20 min
  • 7 steps
Start tutorial Back to the path
Check JavaScript with ESLint

What you will go through

  1. Understand What ESLint Can Check Learn what a lint check can identify in JavaScript source and why browser behavior still needs direct verification. 2 min
  2. Install ESLint and Start a Flat Configuration Install ESLint locally, create a starter ES-module flat configuration, and add the Click Counter files for later diagnostics. 3 min
  3. Match Files to Their Runtime Environment Configure file scopes, ignores, and runtime-specific globals in an ESLint flat configuration. 4 min
  4. Run a Check and Read the Report Run the project-local ESLint command, read diagnostic locations and rules, and relate rule severity to lint-check exit statuses. 3 min
  5. Correct Undefined and Unused Bindings Resolve the Click Counter's undefined and unused bindings by checking code intent, then distinguish the corrected errors from the remaining warning. 3 min
  6. Apply and Review Automatic Fixes Apply a supported ESLint automatic fix, inspect the source change, and verify that Click Counter still behaves as intended. 3 min
  7. Finish with a Lint-and-Behavior Check Complete a local ESLint workflow and verify why a clean lint report is not proof of correct Click Counter behavior. 3 min

What you will learn

  • Install ESLint locally and create a flat configuration for authored JavaScript files.
  • Apply browser and Node.js globals only to the files that run in those environments.
  • Run a lint package script and locate reported problems using file positions and rule names.
  • Correct undefined identifiers and unused bindings, and review changes made by automatic fixes.
  • Explain why a successful lint check still requires manual verification of interface behavior.

Before you start

  • Manage npm Dependencies and Lockfiles
  • Split Browser Code into JavaScript Modules
  • Trace JavaScript Errors with the Browser Debugger

Step 1 of 7

Understand What ESLint Can Check

Learn what a lint check can identify in JavaScript source and why browser behavior still needs direct verification.

Inspecting source versus running an interface

Linting reads code; it does not run it

A lint check reads JavaScript source and compares it with selected rules. This is static analysis: it inspects code without starting the application or activating its interface.

That makes linting useful early. It can identify suspicious patterns while you are editing, before a visitor reaches the affected button or screen.

Two different kinds of checking

Diagram contrasting source-code inspection with interacting with a counter in a browser.

A lint check examines source patterns. A manual behavior check interacts with the running interface.

Useful findings are not a complete verdict

Patterns a lint check can catch

Static analysis can flag a name that is not available where code tries to use it. For example, if a module declares count but an expression reads coutn, the misspelling is a useful finding before anyone activates the counter.

It can also flag an unused binding. If code creates debugLabel and never reads it, that may be leftover code or a sign that intended work is missing. Review the intent rather than assuming every finding has the same repair.

Tip

Important limit

A lint check is not application execution, comprehensive type checking, or proof that the feature fulfills its purpose. It reports patterns covered by its rules; it does not know every requirement a person expects from an interface.

Verify the result in the browser

Correct-looking code can behave incorrectly

Consider a counter whose identifiers are all spelled correctly, but whose click logic subtracts 1 when the interface promise is to add 1. The source can satisfy the selected lint rules and still produce the wrong result.

A manual behavior check answers a different question: does the running interface do what people need? For a counter, that means checking its starting display and activating the control to confirm each result.

Behavior must be observed

Comparison showing a counter correctly changing from 0 to 1 and incorrectly changing from 0 to negative 1 after an activation.

A lint check may find no suspicious source pattern even when the observed counter result is wrong.

Check your distinction

Classify the evidence

Match each observation to the kind of check that can establish it.

Tap an item and then its matching pair.

Step 2 of 7

Install ESLint and Start a Flat Configuration

Install ESLint locally, create a starter ES-module flat configuration, and add the Click Counter files for later diagnostics.

Add ESLint to the project

Install the local tools

From your project root—the folder containing package.json—install the lint tools as project-local development dependencies. This keeps the tool version recorded with the project instead of relying on a global installation.

Use the matching ESLint 9 packages shown here. globals is installed now because the configuration will use it in the next step.

Install ESLint packages

Run this command in your project root.

bash
npm install --save-dev eslint@9 @eslint/js@9 globals
Expected output
added 86 packages, and audited 87 packages in 3s

22 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

Package counts, audit details, and timing vary by project and npm version. The important result is that the packages are added as development dependencies in package.json.

Scope the recommended rules

Create the starter flat configuration

Create eslint.config.mjs in the project root, beside package.json. The .mjs extension tells Node.js to load this configuration as an ES module, regardless of the package type setting.

A flat configuration is a plain array. For now, it imports the recommended JavaScript rules and scopes them to your authored source files and this configuration file. Environment-specific settings come next.

eslint.config.mjs

Save this as eslint.config.mjs in the project root.

javascript
import js from "@eslint/js";

export default [
  {
    ...js.configs.recommended,
    files: ["src/**/*.js", "eslint.config.mjs"]
  }
];

Where the configuration belongs

A project directory tree showing eslint.config.mjs next to package.json at the root and JavaScript files inside src/lint-demo.

The configuration is at the project root; the authored JavaScript is under src/.

Add the practice fixture

Set up Click Counter

Create this small practice interface in your own project. It uses the module structure you already know. Do not repair the intentional issues yet: later steps will use them to practice reading diagnostics and reviewing an automatic fix.

lint-demo.html

Create lint-demo.html at the project root.

html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Click Counter</title>
  </head>
  <body>
    <h1>Click Counter</h1>
    <button id="increment-button">Add 1</button>
    <output id="count-output">0</output>
    <script type="module" src="./src/lint-demo/main.js"></script>
  </body>
</html>

src/lint-demo/counter.js

Create src/lint-demo/counter.js.

javascript
export let increment = (count) => {
  const nextCount = coutn + 1;
  return nextCount;
};

src/lint-demo/main.js

Create src/lint-demo/main.js.

javascript
import { increment } from "./counter.js";

const button = document.querySelector("#increment-button");
const output = document.querySelector("#count-output");
const debugLabel = "Click Counter";
let count = 0;

button.addEventListener("click", () => {
  count = increment(count);
  output.textContent = count;
});

Check your setup plan

Reconstruct the starter setup

From memory, write a short setup note that includes all of the following:

  1. The npm installation command with all three package names.
  2. The import used for the recommended JavaScript rules and the export default array structure.
  3. The exact project location where eslint.config.mjs is saved.

Do not add environment settings yet.

Write at least 120 characters (0/120).

Step 3 of 7

Match Files to Their Runtime Environment

Configure file scopes, ignores, and runtime-specific globals in an ESLint flat configuration.

Separate scopes from exclusions

Scope settings by runtime

A flat configuration is a list of objects. ESLint combines the objects whose files patterns match a file.

A files pattern selects where that object’s settings apply; it does not by itself exclude other files from command-line discovery. Use an ignore-only object when generated output must be excluded.

For this project:

  • src/**/*.js is browser source.
  • eslint.config.mjs is a Node.js ES-module configuration file.
  • dist/** is generated output to ignore.
  • node_modules is ignored by ESLint by default.

Files receive different treatment

A diagram routes browser source to browser settings, the configuration file to Node.js settings, generated output to an ignore path, and dependencies to a default ignored path.

Treat browser source, the Node.js configuration, generated output, and installed dependencies differently.

File treatment map

Use the matching setting only where its runtime supplies those names.

Patterns and ignore behavior in this project
FilesTreatmentReason
src/**/*.jsBrowser globalsRuns in the browser
eslint.config.mjsNode.js globalsRuns as a Node.js module
dist/**IgnoreGenerated output
node_modulesIgnored by defaultInstalled dependencies

Consolidate the flat configuration

Declare only real environment names

Keep the recommended rules in one scoped object, then add separate matching objects for each runtime. This makes the browser and Node.js boundaries visible in the configuration.

Declaring globals tells ESLint which names an environment provides. It does not create those values. For example, Node.js may provide process; a browser does not gain process merely because a configuration lists it.

Complete eslint.config.mjs

Save this complete flat configuration as eslint.config.mjs in the project root. It scopes recommended rules to authored source and the configuration file, ignores generated output, and keeps runtime globals separate.

javascript
import js from "@eslint/js";
import globals from "globals";

export default [
  {
    ignores: ["dist/**"]
  },
  {
    ...js.configs.recommended,
    files: ["src/**/*.js", "eslint.config.mjs"]
  },
  {
    files: ["src/**/*.js"],
    languageOptions: {
      globals: globals.browser
    }
  },
  {
    files: ["eslint.config.mjs"],
    languageOptions: {
      globals: globals.nodeBuiltin
    }
  }
];

Warning

Keep the boundary useful

Do not put Node.js globals in the browser-source object. If browser code accidentally uses process, ESLint should be able to report that the identifier is not defined instead of treating it as a browser-provided value.

Check your scope decisions

Apply the runtime boundary

Apply the configuration decisions to this project. Write a short response with:

  1. The treatment for each category: src/**/*.js, eslint.config.mjs, dist/**, and node_modules.
  2. The exact globals value for browser source and for the configuration file.
  3. Why declaring process for src/**/*.js would hide a useful diagnostic.

Write at least 180 characters (0/180).

Step 4 of 7

Run a Check and Read the Report

Run the project-local ESLint command, read diagnostic locations and rules, and relate rule severity to lint-check exit statuses.

Make linting repeatable

Add a repeatable lint check

A lint check should be repeatable for everyone working in the project. Add a lint entry to the existing scripts object in package.json; keep the existing dev entry. When you run an npm script, npm makes the project-local ESLint executable available.

The script explicitly checks authored source in src and eslint.config.mjs. In the browser-source configuration object, add prefer-const at warning severity. The separately scoped recommended configuration remains unchanged, so no-undef and no-unused-vars remain errors.

package.json scripts

Keep your existing development-server script and add the lint script.

json
{
  "scripts": {
    "dev": "vite",
    "lint": "eslint src eslint.config.mjs"
  }
}

Browser-source configuration object

Add the rules property to the existing object that matches browser source files.

javascript
{
  files: ["src/**/*.js"],
  languageOptions: {
    globals: globals.browser
  },
  rules: {
    "prefer-const": "warn"
  }
},

Run and inspect the report

Read a diagnostic as a source location

From the project root, run the lint check. Read each diagnostic from left to right: file path, line:column, severity, message, and rule. The position identifies the relevant source location; the rule identifies the configured check that reported it. A diagnostic is not an instruction to disable that rule.

Anatomy of an ESLint diagnostic

Diagram connecting the parts of an ESLint report row to a location in a JavaScript source file.

Use the path and line:column to locate code, then use the rule name to understand the diagnostic.

Lint check and expected report

Run this command in your project root. This report matches the intentionally flawed Click Counter files created earlier.

shell
npm run lint
Expected output
> click-counter@1.0.0 lint
> eslint src eslint.config.mjs

/project/src/lint-demo/counter.js
  1:12  warning  'increment' is never reassigned. Use 'const' instead.  prefer-const
  2:21  error    'coutn' is not defined.                                no-undef

/project/src/lint-demo/main.js
  5:7  error  'debugLabel' is assigned a value but never used.  no-unused-vars

✖ 3 problems (2 errors, 1 warning)

Your package name, version, and absolute project path can differ. The file-relative positions, severities, messages, and rule names are the important parts.

Connect severity to exit status

Severity changes the command result

A rule setting controls whether ESLint reports a pattern and the severity it assigns. Investigate the source before changing a setting: hiding a diagnostic does not establish that the code expresses the intended behavior.

For this lint command, one or more errors make the lint check fail. Warnings remain useful diagnostics, but warnings alone allow a successful check.

Rule settings

The configuration uses string values for severity.

The report label follows the configured setting.
SettingWhat ESLint doesReport label
"off"Does not report the ruleNone
"warn"Reports without failing this lint check by itselfwarning
"error"Reports and makes this lint check failerror

Exit status

Check the terminal process result as well as visible diagnostics.

An exit status communicates the lint-check result to the terminal or another tool.
Exit statusMeaning for this lint command
0Successful check; warnings may still be reported
1One or more lint errors were reported
2A configuration or internal ESLint failure occurred

Complete the package script

Complete the value of the lint script without changing the existing development script.

json
{
  "scripts": {
    "dev": "vite",
    "lint": ""
  }
}

Practice a complete report

Summarize the lint report

Write a short lint-check summary for the current Click Counter fixture. Include:

  1. The exact value of the lint script.
  2. Each diagnostic’s file path, line:column, severity, and rule name.
  3. The exit status for a warning-only check, a check with reported lint errors, and a configuration or internal tool failure.

Describe the report only; do not propose code repairs yet.

Write at least 180 characters (0/180).

Step 5 of 7

Correct Undefined and Unused Bindings

Resolve the Click Counter's undefined and unused bindings by checking code intent, then distinguish the corrected errors from the remaining warning.

Investigate the reported names

Read each diagnostic as an intent question

Two errors in Click Counter need different investigations:

  • A no-undef diagnostic asks whether a name is misspelled, outside its scope, or missing from an existing import.
  • A no-unused-vars diagnostic asks whether the binding is unnecessary or whether intended code that should use it is missing.

Do not make a diagnostic disappear by declaring an invented browser global or weakening a rule. Read the nearby code first and determine what the program is intended to do.

Two different investigations

A diagram contrasts a misspelled identifier that should refer to a function parameter with an isolated unused declaration.

Check an undefined name against local scope and imports; check an unused binding against the feature it was meant to support.

Undefined identifier at 2:21

Current src/lint-demo/counter.js

javascript
export let increment = (count) => {
  const nextCount = coutn + 1;
  return nextCount;
};

Unused binding at 5:7

Current src/lint-demo/main.js

javascript
import { increment } from "./counter.js";

const button = document.querySelector("#increment-button");
const output = document.querySelector("#count-output");
const debugLabel = "Click Counter";
let count = 0;

button.addEventListener("click", () => {
  count = increment(count);
  output.textContent = count;
});

Repair the errors, then rerun

Make intent-preserving edits

At src/lint-demo/counter.js 2:21, coutn is not declared in the function, imported, or supplied by the browser. Compare its spelling with the local parameter and repair the reference to the intended current value.

At src/lint-demo/main.js 5:7, debugLabel is a plain string that no code reads. It supports no missing feature and its initialization has no meaningful side effect, so remove that declaration. In other code, check an initializer before deleting it: calling a function can perform meaningful work even when its returned binding is unused.

Corrected src/lint-demo/counter.js

Replace only the misspelled reference. Keep the let declaration for increment unchanged for now.

javascript
export let increment = (count) => {
  const nextCount = count + 1;
  return nextCount;
};

Corrected src/lint-demo/main.js

Remove only the unnecessary declaration.

javascript
import { increment } from "./counter.js";

const button = document.querySelector("#increment-button");
const output = document.querySelector("#count-output");
let count = 0;

button.addEventListener("click", () => {
  count = increment(count);
  output.textContent = count;
});

Remaining diagnostic after the two repairs

Save both files and run the lint check from the project root.

shell
npm run lint
Expected output
src/lint-demo/counter.js
  1:12  warning  'increment' is never reassigned. Use 'const' instead  prefer-const

✖ 1 problem (0 errors, 1 warning)

The npm script header can vary with your package name and npm version. The important result is one warning and zero errors.

Decide what the unused binding means

Choose a response to an unused binding

A binding stores a plain label string, no code reads it, and no missing feature needs it. What is the appropriate response?

Apply the diagnostic evidence

Report the repair result

In your own editor, make the intended repairs and run npm run lint. Then write a concise report that includes:

  1. The identifier replacement you made in src/lint-demo/counter.js.
  2. What you did with debugLabel in src/lint-demo/main.js, and why.
  3. The rule and severity of the remaining diagnostic, plus whether this warning-only lint check succeeds or fails.

Use the diagnostic evidence to justify the edits; do not propose changing the rules to hide either error.

Write at least 120 characters (0/120).

Step 6 of 7

Apply and Review Automatic Fixes

Apply a supported ESLint automatic fix, inspect the source change, and verify that Click Counter still behaves as intended.

Forward a supported fix option

What an automatic fix can—and cannot—do

An automatic fix is a source edit ESLint knows how to make for a supported diagnostic. npm’s -- separator forwards the following option to the ESLint command in the lint script.

A fix does not run Click Counter, infer the intended meaning of a misspelled identifier, or prove that the interface works. Because it writes to local files, inspect every edit before treating the result as trustworthy.

One source edit, then a separate behavior check

A diagram showing a terminal-triggered linting tool changing one declaration keyword in a code editor while a separate counter interface still requires manual inspection.

An automatic fix can make a supported code edit; a manual behavior check is still a separate task.

Practice the command

Request an automatic fix

Complete the command that requests supported automatic fixes from ESLint.

shell
npm run lint -- 

Inspect, rerun, and verify

Inspect the current source first

src/lint-demo/counter.js — before the automatic fix

javascript
export let increment = (count) => {
  const nextCount = count + 1;
  return nextCount;
};

Compare the focused edit

src/lint-demo/counter.js — expected source after the automatic fix

javascript
export const increment = (count) => {
  const nextCount = count + 1;
  return nextCount;
};

Rerun the check, then verify Click Counter

Run npm run lint -- --fix from the project root, then open src/lint-demo/counter.js in your editor. Compare the saved file with the two versions above: only the declaration of increment should change from let to const; its parameter and function body stay the same.

Next run npm run lint again. With the earlier spelling and unused-binding repairs saved, the Click Counter fixture should produce no diagnostics. If your project contains additional authored files, investigate any diagnostics they report rather than assuming every finding was automatically repairable.

Then run npm run dev. Open the local origin reported by Vite with /lint-demo.html appended; the port may vary. Perform a manual behavior check: the display starts at 0, the first activation of Add 1 displays 1, and the second displays 2. Use a pointer for one activation and the button’s native keyboard activation for the other.

Apply the complete verification cycle

Record the fix-and-review cycle

Perform the full local cycle. In a concise report, include:

  1. The complete command used to request automatic fixes.
  2. The exact declaration edit you inspected in src/lint-demo/counter.js.
  3. The ordered sequence: fix, inspect, rerun the lint check, and manually verify behavior.
  4. What your rerun reported and what you observed at /lint-demo.html initially, after the first activation, and after the second activation.
  5. Why an undefined identifier or an unclear logic change may still need manual judgment instead of an automatic fix.

Write at least 220 characters (0/220).

Step 7 of 7

Finish with a Lint-and-Behavior Check

Complete a local ESLint workflow and verify why a clean lint report is not proof of correct Click Counter behavior.

Use both kinds of verification

One repeatable local loop

Use the same local loop each time you change the Click Counter:

  1. Confirm that src/**/*.js receives browser globals, eslint.config.mjs receives Node.js globals, and dist/** is ignored.
  2. Run the lint check.
  3. Investigate any diagnostic and make an intentional edit.
  4. Inspect the edit, then rerun the lint check.
  5. Start the existing development server and perform a manual behavior check in the browser.

The first four steps examine configured source rules. The last step checks what a person can actually do with the interface.

From source rules to interface behavior

A four-stage workflow moves from scoped project files to a terminal check, inspected code edit, and browser interaction.

A lint check is one stage of the workflow; the browser interaction is a separate manual behavior check.

See the limit of a clean report

A lint-clean logic defect

A clean report can still accompany the wrong interface behavior. To prove it, temporarily use this valid version of src/lint-demo/counter.js after completing the earlier repairs. Every identifier is defined and used, so the configured lint rules have no reason to report it. Yet the calculation moves the counter in the opposite direction from the button label.

Temporary incorrect variation

src/lint-demo/counter.js

javascript
export function increment(count) {
  const nextCount = count - 1;
  return nextCount;
}

Run the lint check

Run this from the project root after saving the variation.

bash
npm run lint
Expected output
> your-project@1.0.0 lint
> eslint src eslint.config.mjs

The project name and version in the first line vary. With no diagnostics, ESLint normally prints nothing after the command line and exits with status 0.

Summary

Final workflow review

  • Confirm scopes before interpreting a report.
  • A clean lint check means no configured rules reported a problem.
  • Inspect edits and use a manual behavior check to verify the intended interface task.

Correct the intended behavior

Repair the calculation

Make the smallest correction so increment matches the Click Counter’s intended behavior.

javascript
export function increment(count) {
  const nextCount = count  1;
  return nextCount;
}

Apply the complete cycle

Final lint-and-behavior report

Perform the complete final check in your own project. In one concise report:

  • state which files receive browser globals, which file receives Node.js globals, and what is ignored;
  • give the exact lint command and report whether it completed with no diagnostics;
  • identify the lint-clean defect in the temporary variation;
  • after correcting it, report the initial displayed value, the value after one pointer activation, and the value after one keyboard activation; and
  • explain why this result still requires a manual behavior check after a successful lint check.

Write at least 220 characters (0/220).

Tutorial complete

Congratulations! You completed: Check JavaScript with ESLint

You completed a repeatable lint-and-behavior check for a browser interface.

Download the App now to have access to + 5000 free courses, exercises, certificates and lots of content without paying anything!

  • 100% free online courses from start to finish

    Thousands of online courses in video, ebooks and audiobooks.

  • More than 60 thousand free exercises

    To test your knowledge during online courses

  • Valid free Digital Certificate with QR Code

    Generated directly from your cell phone's photo gallery and sent to your email

Cursa app on the ebook screen, the video course screen and the course exercises screen, plus the course completion certificate