JAVASCRIPT / MODULES AND DEVELOPER TOOLING
Linting, formatting, and catching bugs early
Separate lint rules from formatting rules, read lint reports by rule id, and recognize the bug classes static analysis can prove before the code runs.
What you will learn
- Tell semantic lint rules apart from layout rules and give each to the right tool
- Read a lint report as line:column, severity, message, rule id, then look up the rule
- Name bug classes lint proves statically: undeclared names, dead code, duplicate keys
- Silence a rule only with a scoped eslint-disable-next-line plus a stated reason
Understanding Linting, formatting, and catching bugs early
A formatter and a linter both begin by parsing your file into a syntax tree, then do opposite things with it. A formatter throws the original whitespace away and prints the tree back out under fixed rules, which is why its output cannot depend on how you indented, and also why it can never tell you a name is misspelled. A linter keeps the tree and walks it with many small independent rules, most of which also consult a scope map built from every declaration in the file, so it can answer questions about meaning: is this binding declared anywhere, is it ever read, is this statement reachable.
Static analysis catches a different class of defect than running the code, and the reason is control flow. Running a file only exercises the paths your inputs reach, while a rule like no-undef inspects every return statement in a function whether or not anything ever calls it with a 503. The same property marks the boundary of the tool: a rule reads the file, so it cannot know whether the object your API returned has an email property, and it will happily approve price * height when you meant price * quantity, because both names are declared. That is why linting sits next to tests and type checking rather than replacing them.
The two tools overlap on quotes, semicolons and indentation, and that overlap is where setups go wrong: when a stylistic lint rule and the formatter disagree, each rewrites the other's output and the file never stabilizes. The usual resolution is to remove stylistic rules from the lint config, let the formatter own layout, and leave lint owning correctness. Then run the same rules at three moments of rising cost, as editor markers while you type, on staged files before a commit, and as a branch gate, because a report you see while typing costs seconds and the same report on a broken main branch costs a morning.
function statusLabel(code) {
if (code < 400) {
return "ok";
}
if (code < 500) {
return "client error";
}
return serverLabel; // never declared anywhere: this is what no-undef reports
}
const seen = [];
for (const code of [200, 404]) {
seen.push(statusLabel(code));
}
console.log(seen.join(", "));
try {
console.log(statusLabel(503));
} catch (err) {
console.log(err.name + ": " + err.message);
}A linter reasons about what code means on every branch without executing it, while a formatter only decides how that code is printed.
Worked examples
A lint rule is just code that reads code
Builds a one-rule linter over source text to show where a report's line, column and rule id come from.
const source = [
"function total(items) {",
" let sum = 0;",
" for (const item of items) {",
" console.log(item);",
" sum += item.price;",
" }",
" return sum;",
"}"
].join("\n");
const problems = [];
source.split("\n").forEach((text, index) => {
const at = text.indexOf("console.");
if (at !== -1) {
problems.push({ line: index + 1, column: at + 1, rule: "no-console" });
}
});
for (const p of problems) {
console.log(p.line + ":" + p.column + " warning Unexpected console statement " + p.rule);
}
console.log(problems.length + " problem, 0 errors");Example explained
Line 1source is a string, so the console.log(item) inside it is data that is never executed.
Line 2indexOf returns a zero-based offset while reports are one-based, hence at + 1 for the column.
Line 3A real rule walks a syntax tree, so it ignores a console. that appears inside a string or comment; this line-based search would report it as a false positive.
Line 4The printed shape, line:column severity message rule-id, is exactly what a command line lint run gives you.
Formatting reveals a bug the parser already made
Shows how a newline after return changes the meaning of the code, which is why lint flags the block below as unreachable.
function makeConfig() {
return
{
debug: true
}
}
function makeConfigFixed() {
return {
debug: true
}
}
console.log(makeConfig());
console.log(makeConfigFixed().debug);Example explained
Line 1A line break directly after return triggers automatic semicolon insertion, so makeConfig returns undefined before reaching the braces.
Line 2The braces below then parse as a block statement, where debug: is a label and true is an expression, which is legal syntax and throws nothing.
Line 3no-unreachable reports that block, and a formatter reprints it as return; followed by a detached block, making the split visible in the source.
Line 4makeConfigFixed keeps the opening brace on the return line, so the object literal is part of the returned expression.
A silent overwrite the runtime will not report
Demonstrates the duplicate object key that no-dupe-keys catches and that no error message ever mentions.
const settings = {
retries: 3,
timeout: 1000,
retries: 5
};
console.log(settings.retries);
console.log(Object.keys(settings).join(", "));Example explained
Line 1The later retries entry overwrites the earlier one while the object is built, so the 3 is gone and unrecoverable.
Line 2The property keeps its original insertion position, which is why retries still prints before timeout.
Line 3ES5 strict mode rejected duplicate data keys, ES6 legalized them, so modern engines stay silent and no-dupe-keys is the only thing that objects.
Important notes
no-undef only knows about the globals it is told exist, so window, process or describe must come from the declared environment or you get reports for code that runs fine.
Autofix is safe for rules like prefer-const but does nothing for no-unused-vars, which is left alone because deleting code could delete side effects; review the diff of a large autofix run instead of committing it unseen.
Common mistakes
Leaving stylistic lint rules on alongside a formatter: the lint autofix and the formatter rewrite each other's output, so the file never settles and the gate fails on code that was just formatted.
Answering a report with eslint-disable-next-line no-unused-vars over a misspelled import: the import stays broken, the comment outlives its reason, and the next reader assumes it was deliberate.
Reading a clean lint run as proof the code is correct, when lint says nothing about total = price * height where quantity was meant, because both names are declared.
Try it yourself
Change, predict, then run
In a browser editor, write an object literal that sets timeout twice and a function whose return sits alone on the line above an object literal. Predict both printed values before running, then name the lint rule that would have caught each one.
Open the JavaScript workspaceCheck your understanding
Which of these questions can a lint rule decide with certainty by reading the file alone?
- Whether the name serverLabel is declared in any scope the return statement can see
- Whether the object that fetchUser() resolves to has an email property
- Whether total becomes NaN for the inputs real users send
- Whether the retry loop eventually terminates in production
Show answer
Declarations and the scopes they belong to are visible in the source itself, so scope analysis settles the first question without executing anything, which is what no-undef does. The second option feels checkable because editors underline missing properties, but that comes from a type checker reading declared types, not from a rule reading your file; the real shape of a fetch result exists only at runtime, as do the inputs and loop behavior in the other two.