Linting with ESLint
Use ESLint as an automatic code proofreader: understand rules, configs, editor squiggles, fix-on-save, and which warnings to treat as errors.
- 01Read a lint messageFind the rule id, the line, the severity, and the fix hint in an ESLint message.
- 02Set rules in a configRead a small ESLint config file, choose a severity for each rule, and name a few useful rules.
- 03Use ESLint in your editorKnow how squiggles, quick fixes, and fix on save work in VS Code.
What a linter does
A linter is a program that checks your code for likely mistakes. It does this before you run the code. ESLint is the most widely used linter for JavaScript. It can find things like these:
- a misspelled variable name;
- a comparison your team chose not to use, like
==; - code after a
return, which can never run; - a value you made and then never used.
ESLint only reads your code. It doesn't run your functions or click around your page. It reads the file and checks it against a list of rules. A rule is one named check, such as "no unknown names". When a rule finds a problem, ESLint gives you a message. Each message tells you three things:
- the rule id: the rule's name, like
no-undef; - the line and column: where the problem is in the file;
- the severity: how serious the problem is, either a warning or an error.
A spell checker marks a word that looks wrong before you send your message. A linter does the same for code. It marks names, operators, and team rules that look wrong, before you run anything.
- In real life: A red line under a typo
- In JavaScript: A lint message about
totla - In real life: A grammar rule: end with a period
- In JavaScript: An ESLint rule: use
=== - In real life: Autocorrect fixes a simple typo
- In JavaScript:
--fixmakes safe changes, likelettoconst - In real life: Some sentences need a person to fix them
- In JavaScript: Some bugs need tests and debugging
You met strict mode, semicolons, and coding style in earlier lessons. A linter checks those habits for you while you type. You'll see two commands in this lesson. The command npm init @eslint/config@latest sets ESLint up, and the command npx eslint runs it. A later lesson explains npm and package.json. For now, you only need to know what these two commands do.
const userName = "Asha";console.log(userName);Line 1 creates a variable called userName and stores "Asha" in it. Line 2 prints that variable, so you see Asha. Now imagine that line 2 says userNmae instead. No variable has that name. The no-undef rule ("no undefined names") reports it before you run the code.
The picture and the step-through below check a different two-line file. Its line 2 has a typo: totla instead of total.
no-undef), where the problem is (line 2, column 13), and how serious it is (an error).Pick the correct or the misspelled version. These results were saved from a real ESLint run, and this lesson's tests check them.
script
console.log(totla);Linting helps even when the code runs fine. The next example prints Welcome with no error. But the no-unused-vars rule ("no unused variables") points at line 1. The name made there is never used. Often that means you forgot to show it, or forgot to delete it.
const userName = "Asha";console.log("Welcome");Line 1 creates userName. Line 2 prints only the word Welcome. So the output is Welcome, and userName is never used. To fix the warning, use the name.
const userName = "Asha";console.log("Welcome", userName);Line 1 is the same as before. Line 2 now prints userName too, so the output is Welcome Asha. The name is used, so no-unused-vars has nothing to report.
Lint this code: rules report messages
INTERACTIVEThe list that ESLint prints is like a checklist. Each item is one message, and each message names the rule that found it. In the playground below:
- Pick a snippet with the buttons at the top.
- Turn rules on and off with the rule buttons.
- Read the messages. You see the messages from every rule that is on, all in one list.
For example, the "Loose equality" snippet uses ==. When the eqeqeq rule is on, ESLint reports it. The name eqeqeq means "equals, equals, equals": always use ===.
if (score == "10") { console.log("match");}2 saved ESLint messages. Each message below shows the rule id, the severity, the line, the column, and whether ESLint can fix it safely.
To read a message, ask four questions:
- Which rule found it? The rule id tells you.
- Where is it? The line and column tell you.
- How serious is it? The severity tells you: warning or error.
- Can ESLint fix it for you? Some rules can make a safe change by themselves.
You can search for any rule id online. The id no-undef means "a name was used but never created". The id prefer-const means "this let never changes, so use const".
This page doesn't run ESLint in your browser. ESLint is too big to load into a lesson page. Instead, the lesson stores results from a real ESLint run. The lesson's tests run ESLint 9's Linter and check that every stored line, column, rule id, message, severity, and fix flag still matches.
ESLint rules and configs
RULEBOOKA rule is one check, like "always use ===". A config is a file that lists which rules to use. ESLint reads the config before it checks your files. ESLint 9 looks for a config file called eslint.config.js. This kind of config is called a flat config, because it exports one plain list of settings. Most configs start with ESLint's recommended rules, then add a few choices of their own.
A family writes its house rules on one list and sticks it on the fridge. Then nobody has to argue about them again. An ESLint config is that list for your project's code.
- In real life: Take your shoes off at the door
- In JavaScript:
eqeqeq: use===instead of== - In real life: Be quiet after 10 pm
- In JavaScript:
no-console: warn aboutconsole.login app code - In real life: The list of house rules on the fridge
- In JavaScript:
eslint.config.js
import js from "@eslint/js"; export default [ js.configs.recommended, { languageOptions: { globals: { document: "readonly", console: "readonly" } }, rules: { eqeqeq: "error", "no-unused-vars": "warn" } }];Line by line:
- Line 1 loads ESLint's recommended rules from the
@eslint/jspackage. - Line 4 adds those recommended rules to the config list.
- Lines 8 and 9 tell ESLint that
documentandconsolealready exist, because the browser provides them. - Lines 13 and 14 turn on two rules and say how serious each one is.
ESLint reads this file in Node.js when it starts. The file never runs in your web page.
Each rule gets a severity. You can write it as a word or as a number:
"off"or0: the rule is turned off."warn"or1: ESLint shows a warning."error"or2: ESLint shows an error.
A warning is like a sticky note: worth reading, but your work can go on. An error is like a stop sign. When npx eslint finds an error, the check fails. Teams often run the same check automatically before new code joins the shared project. That automatic check is called CI, short for "continuous integration". An error makes CI fail too. Use errors for real bugs and for rules your team never wants broken.
"error" makes npx eslint fail. A warning is shown, but the check still passes.Warnings don't make the check fail. If you want warnings to fail it too, run npx eslint . --max-warnings 0. This is a choice each project makes. It doesn't change any rule.
The table below lists the config words you'll see most. Read each row as: the word, what it means, and an example.
| Word | What it means | Example |
|---|---|---|
| Rule | One named check. | no-undef reports names that were never created. |
| Severity | How serious a rule's message is. | warn is a gentle note. error makes the check fail. |
| Recommended config | A ready-made set of rules. | js.configs.recommended comes from the @eslint/js package. |
| Globals | Names that already exist without your file creating them. | Browser code often lists document and console as readonly. Many teams use the globals package for this list. |
| Disable comment | A comment that turns a rule off in one place. | // eslint-disable-next-line no-alert, followed by a short reason. |
A global is a name your file can use without creating it, because the place where the code runs provides it. In a browser, document is the web page. Node.js has no web page, so it has no document. Tell ESLint where your code runs. Then it won't report real names like document as typos. Mark these names readonly, unless your code is meant to replace them.
Sometimes one line has to break a rule on purpose. Then turn the rule off for that one line with a disable comment, and write the reason next to it. Don't turn the rule off for the whole project. The reason is a note for the next person who reads the code.
const score = 10;// eslint-disable-next-line eqeqeq -- legacy form valueif (score == "10") console.log("Ready");Line by line:
- Line 1 stores the number 10.
- Line 2 is the disable comment. It turns off
eqeqeqfor the next line only. The text after--is the reason: the value comes from an old form. - Line 3 uses
==on purpose and printsReady.
Keep an exception this small. Suppose line 3 changes later and no longer breaks the rule. Then ESLint can tell you that the comment isn't needed any more, if that check is turned on in the config.
Fixing on save
STEP THROUGHESLint can fix some problems by itself. A rule that can do this is called fixable. ESLint only makes a fix when it is sure the fix is safe. Take prefer-const. If a variable is made with let and never gets a new value, ESLint can change let to const. The program still prints the same thing.
Other rules need you to decide. If you typed totla, the no-undef rule can't know what you meant. You might have meant total, or you might want a new variable. So ESLint reports the problem and leaves the fix to you.
--fix makes it. On the right, ESLint can't tell which name you meant, so it only reports the problem and you fix it.Step through one real fix. The lesson saved the fixed code from ESLint, and the tests check it against ESLint.
script
console.log(limit);Don't change every let to const. Use let when the variable really changes. In the next example, line 2 gives score a new value. ESLint sees that, so prefer-const reports nothing. The program prints 2.
let score = 1;score = 2;console.log(score);Line 1 sets the score to 1. Line 2 changes it to 2. Line 3 prints 2. The change on line 2 is on purpose, so let is the right choice. A lint rule is feedback about this code. It isn't an order to use one keyword everywhere.
You can also run ESLint in a terminal: the window where you type commands. The command npx eslint . checks the whole project and lists the problems. The dot means "this folder". The command npx eslint . --fix does the same, and also makes the safe fixes. The npm lesson explains npx later. For now, read it as "run ESLint on this project".
npm init @eslint/config@latestnpx eslint .npx eslint . --fixnpx eslint . --max-warnings 0Line by line:
- Line 1 starts ESLint's setup helper, which creates a config file for you.
- Line 2 checks every file in the folder and lists the problems.
- Line 3 does the same, and also makes the safe fixes.
- Line 4 makes the check fail if even one warning is left.
ESLint inside your editor
INTERACTIVEESLint helps most when you see its messages while you type. In VS Code, install the ESLint extension. An extension is an add-on for the editor. The ESLint extension does four things:
- It reads your project's config.
- It draws a wavy line, called a squiggle, under each problem.
- It offers quick fixes that you can click.
- It can make the safe fixes each time you save the file.
To fix on save, add the setting below to your VS Code settings. Recent versions of VS Code use the value "explicit". It means "fix when I save the file myself", not when the editor saves by itself. Older examples use true instead.
{ "editor.codeActionsOnSave": { "source.fixAll.eslint": "explicit" }}let limit = 3;console.log(limit);Saving can change let limit to const limit, because ESLint checked that limit never gets a new value.
With this setting, the VS Code ESLint extension makes ESLint's safe fixes when you save the file. Recent VS Code uses explicit. Older examples sometimes show true.
{ "editor.codeActionsOnSave": { "source.fixAll.eslint": "explicit" }}Line 3 is the setting that matters. The name source.fixAll.eslint means "make all of ESLint's safe fixes".
The extension doesn't replace learning the rules. When you see a squiggle, read the message and look at the line. Then choose one of these:
- accept the quick fix;
- change the code yourself;
- or, if the line must break the rule, add a disable comment with a reason.
With that habit, lint messages teach you something each time.
Linter or formatter?
SORTThe last lesson showed Prettier, which is a formatter. A formatter fixes how code looks: spaces, line breaks, and indentation. Then nobody has to argue about layout. A linter looks for code that is probably wrong, such as:
- names that are never used;
- names that were never created;
- code that can never run;
- code that breaks a rule your team chose.
The table compares the two tools. Read each row as: the tool, what it checks, and what it does.
| Tool | What it checks | What it does |
|---|---|---|
| Formatter | How the code is laid out. | It rewrites spaces, line breaks, quotes, and the comma after the last item in a list. |
| Linter | Whether the code breaks a rule. | It reports a message. Sometimes it can also make a safe fix. |
| Both together | Whether the whole team writes code the same way. | Prettier handles layout, and ESLint handles likely mistakes. |
- A variable is created but never used
- The indentation is uneven
- The code uses
==, but the team wants=== - A line is too long to read easily
- A variable is used before the line that creates it
- A list that spans several lines has no comma after its last item
- An extra semicolon after a statement
Put each problem under the tool that usually helps first. Some teams set the tools up differently, but these are good choices to start with.
Where you'll use linting
In a real project, ESLint runs in three places:
- In your editor, which shows squiggles while you type.
- In a terminal, where you check the whole project before you share your work.
- In CI, the automatic check that runs before code joins the shared project.
Imagine you are building a small shopping-cart page. A linter can't tell you whether the checkout page looks good. But it can catch mistakes like these:
cartTotaltyped ascartTtoal;- code after a
return, which never runs; - two keys in one object with the same name, where the second one quietly replaces the first.
const limit = 3;console.log(limit);This is the code after the prefer-const fix. Line 1 uses const, because limit never changes. Line 2 prints 3. The fix changed line 1, but the output is still 3.
const cart = { total: 12 };if (cart.total == 0) { console.log("Your cart is empty");} else { console.log("Ready to pay");}Line 1 makes a cart with a total of 12. Line 2 checks whether the total is 0. It isn't, so the else branch runs and prints Ready to pay. The code works, but the eqeqeq rule still reports the == on line 2. Exercise 6 asks you to fix it.
const cart = { total: 12 };if (cart.total === 0) { console.log("Your cart is empty");} else { console.log("Ready to pay");}Only the operator changed, from == to ===. The total is still 12, so the code still prints Ready to pay. Before it compares, == can quietly turn a value into another type. For example, "0" == 0 is true, because the text "0" becomes the number 0. The === operator never does that. That's why many teams turn on eqeqeq.
function getDrink() { return "tea"; console.log("not reached");} console.log(getDrink());Line 2 ends getDrink right away and gives back "tea". Line 3 comes after the return, so it can never run. The no-unreachable rule reports it. Line 6 prints tea. If you need line 3, move it above the return. If you don't, delete it.
const settings = { theme: "dark", theme: "light",};console.log(settings.theme);The object has two properties named theme. JavaScript keeps the last one, so line 5 prints light. This is allowed JavaScript, so nothing crashes. But it is easy to miss when you read the code. The no-dupe-keys rule ("no duplicate keys") catches it.
const lintMessages = ["no-undef"];const shouldMerge = lintMessages.length === 0;console.log(shouldMerge ? "Ready to merge" : "Fix lint messages first");console.log("First rule:", lintMessages[0]);This code pretends to be a CI check. Line by line:
- Line 1 is a list of lint messages. It holds one rule id:
no-undef. - Line 2 checks whether the list is empty. It isn't, so
shouldMergeisfalse. - Line 3 prints
Fix lint messages first, becauseshouldMergeisfalse. - Line 4 prints the first rule:
First rule: no-undef.
A real CI check runs ESLint itself. But the idea is the same: an error stops the code from joining the shared project.
- Write a small piece of code.
- Read any squiggles before you run the page.
- Use the safe quick fixes, but make sure you understand each one.
- Run
npx eslint .before you call the work done. - If a rule seems wrong, ask: should the config change, or should my code change?
Common misconceptions
"If lint passes, the app works."
Passing lint only means the rules that are on found no problem. You still need to run the code, test what it does, and think it through.
"Every lint message is a bug."
Some messages are about style, or about code that will be hard to change later. Check the severity and the rule id to see how urgent a message is.
"An autofix means ESLint understands my goal."
An autofix only means the rule's author marked that change as safe. ESLint doesn't know what your app is supposed to do.
"Formatters and linters do the same job."
They overlap a little. But Prettier mostly fixes layout, and ESLint mostly finds likely mistakes.
"Disable comments are harmless."
Sometimes a disable comment is the right choice. But each one hides a message. Keep each one to a single line, and write why it is there.
const cart = { total: 12 };const discount = 20;console.log(cart.total - discount);ESLint finds no problem in these three lines. But line 3 prints -8: a discount of 20 taken off a cart total of 12. Can a discount be bigger than the total? ESLint can't know that. Your team decides that rule, and tests check it.
Practice: make ESLint useful
6 EXERCISESWhich rule reports the misspelled name in this code?
let count = 1;
console.log(cout);The code creates count but prints cout. No variable called cout exists, so ESLint reports no-undef.
Change the code so the prefer-const rule has nothing to report. Then type what the fixed program prints.
let total = 1 + 2;
console.log(total);const total = 1 + 2;
console.log(total);Changing let to const fixes the prefer-const message. 1 + 2 is still 3, so console.log(total) still prints 3.
In an ESLint config, make the eqeqeq rule an error, so that any == fails the check.
import js from "@eslint/js";
export default [
js.configs.recommended,
{
rules: {
eqeqeq: "error"
}
}
];rules: {
eqeqeq: "error"
}Each rule setting is a property inside the rules object. Setting eqeqeq to "error" means any == makes the check fail.
Look at the VS Code setting from this lesson. What value goes after source.fixAll.eslint?
{
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
}
}With the VS Code ESLint extension installed, setting source.fixAll.eslint to "explicit" makes ESLint's safe fixes each time you save the file yourself.
Your team agrees that unknown variable names must never be merged. Should the no-undef rule be a warning or an error?
Choose error for a rule that should make CI fail and stop a merge. Choose warn for advice the team wants to see, but that shouldn't stop work.
A shopping page should say the cart is empty only when the total is 0. The team has turned on the eqeqeq rule. Change the comparison on line 2 so the rule has nothing to report. Then check what the code prints.
const cart = { total: 12 };
if (cart.total == 0) {
console.log("Your cart is empty");
} else {
console.log("Ready to pay");
}const cart = { total: 12 };
if (cart.total === 0) {
console.log("Your cart is empty");
} else {
console.log("Ready to pay");
}Changing == to === fixes the eqeqeq message. The total is still 12, so the else branch runs and prints Ready to pay.
Quiz: check your understanding
7 QUESTIONSTry each question before you open the explanations. Some wrong choices look right on purpose, so read each one carefully.
Question 1 of 7What is a linter's main job?
Choose an answer to see the explanation.
Question 2 of 7Which rule reports this code?
Read the code, then predictconsole.log(totla);Choose an answer to see the explanation.
Question 3 of 7Which severity usually makes a script or a CI check fail?
Choose an answer to see the explanation.
Question 4 of 7What does this program print after ESLint makes the
prefer-constfix?Read the code, then predictlet limit = 3; console.log(limit);Choose an answer to see the explanation.
Question 5 of 7Which file name is the ESLint 9 config file used in this lesson?
Choose an answer to see the explanation.
Question 6 of 7Which tool is most likely to fix uneven indentation by itself?
Choose an answer to see the explanation.
Question 7 of 7Why should disable comments be rare?
Choose an answer to see the explanation.
Key takeaways
- A linter reads your code before it runs and reports anything that breaks a rule.
- Each ESLint message gives a rule id, a severity, a line, a column, and a short message. Some also offer a safe fix.
- A config lists the rules to use. In ESLint 9, the config file is
eslint.config.js. - Use
errorfor problems that must stop the work, andwarnfor advice. - Squiggles and fix on save in your editor let you see and fix problems while you type.
- A formatter fixes layout. A linter finds likely mistakes.
Remember the one-liner.
A linter is an automatic checker for code: it finds likely mistakes and broken team rules before you run the code.
Coming next: Thinking like a developer.