JavaScript (ES2026) Module 3 – Control flow
Conditionals: if, else and the ternary operator
Write JavaScript if and else chains that cover every case, choose a value with the ternary operator, and turn deep nesting into flat guard clauses.
What you will learn
- Write if/else chains that handle every case
- Use the conditional (ternary) operator for simple value choices
- Replace deep nesting with guard clauses
- Fix conditions that ignore the data, such as x = 0 and x === 'a' || 'b'
Before you start
What you’ll learn · Before you start
What you will learn
- Write if/else chains that handle every case
- Use the conditional (ternary) operator for simple value choices
- Replace deep nesting with guard clauses
- Fix conditions that ignore the data, such as x = 0 and x === 'a' || 'b'
Before you start
On this page
Every program has to choose sooner or later: book the seat or join the waiting list, charge one fee or another, show
an error or carry on. In JavaScript, if (condition) { … } executes its block when the condition holds and skips it
otherwise, and an else block attached to it covers the opposite case. Chaining else if checks several conditions
in turn, and only the first one that holds gets its block executed. To pick between two values inside an expression,
there is the conditional operator, condition ? a : b, called the ternary operator because it takes three operands.
This lesson writes each form, shows the mistakes that make a condition give the same answer whatever the data says,
and turns deeply nested checks into flat guard clauses.
if and else
An if statement is the keyword if, a condition in parentheses and the statement to run when the condition holds.
That statement is almost always a block, a group of statements between braces. An else after it adds the
statement for the other case.
function and return
Each decision in the examples sits inside a function so that one program can try it on several inputs.
function book(seatsLeft) { … } names the code and its parameter, book(2) calls it with seatsLeft equal to 2,
and return sends a value back to the caller and stops the function on that line. The functions module, later in
the track, explains all of this properly.
// One decision with two paths: book a seat while any are left, otherwise join the waiting list.
function book(seatsLeft) {
if (seatsLeft > 0) {
console.log(`Booked. Seats remaining after this one: ${seatsLeft - 1}`);
} else {
console.log('Full: added to the waiting list');
}
console.log('(this line runs on both paths)');
}
book(2);
book(0); Output
Booked. Seats remaining after this one: 1 (this line runs on both paths) Full: added to the waiting list (this line runs on both paths)
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node seat-check.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
With 2 seats remaining, seatsLeft > 0 is true, so the first block runs and the else block is skipped; with 0 seats
it is the other way round. The last line of the function sits outside both blocks, so it runs on either path. Press
Run, then Edit, and add book(-1) to see which path a negative number takes.
Always write the braces
The braces are optional when a block would hold a single statement, and leaving them out causes two classic bugs:
// Without braces, an if controls exactly one statement. Indentation means nothing to JavaScript.
const balance = 900;
const price = 800;
if (balance < price)
console.log('Not enough balance');
console.log('Payment refused'); // looks like part of the if, but runs every time
if (balance >= price) {
console.log('Payment accepted');
}
// An else belongs to the nearest if without an else, whatever the indentation says.
const signedIn = false;
const isAdmin = false;
if (signedIn)
if (isAdmin) console.log('Admin tools');
else console.log('Please sign in'); // never printed: this else belongs to if (isAdmin)
console.log('end of program'); Output
Payment refused Payment accepted end of program
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node missing-braces.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
The balance is large enough, yet the program prints “Payment refused” before “Payment accepted”. Without braces, the
if on line 5 controls only the next statement. The indented console.log('Payment refused') after it is an ordinary
statement that runs every time, because JavaScript ignores indentation completely.
The second bug is the dangling else. The language specification settles it with one rule: an else belongs to the
nearest if that does not have an else yet. So else console.log('Please sign in') belongs to if (isAdmin), not
to if (signedIn), and a visitor who is not signed in sees nothing at all. Braces around every block prevent both
bugs, and ESLint’s curly rule reports each body written without them, both bodies of this program included.
What a condition accepts
The condition does not have to be true or false. JavaScript converts whatever value it gets with the same rules as
Boolean(value): false, 0, -0, 0n, NaN, the empty string '', null and undefined count as false, and
every other value counts as true, including '0', a string holding one space, and every array and object, even empty
ones. (Browsers keep one historical exception, the object document.all, which counts as false.) The lesson on
booleans and truthiness goes through these values one by one. For conditions, the consequence is that a short test can
ask a different question from the one you meant:
// An if converts its condition to true or false first, so test what you actually mean.
const order = { items: [], quantity: 0, note: ' ' };
if (order.items) console.log('items: an empty array still counts as true');
if (order.items.length > 0) console.log('not printed: the order has no items');
if (!order.quantity) console.log('quantity: 0 counts as false, like a missing value');
if (order.quantity !== undefined) console.log('quantity was given, and it is 0');
if (order.note) console.log('note: a single space counts as true');
if (order.note.trim() !== '') console.log('not printed: the note is blank'); Output
items: an empty array still counts as true quantity: 0 counts as false, like a missing value quantity was given, and it is 0 note: a single space counts as true
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node truthy-conditions.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
if (order.items) asks “is there an items value?”, not “are there any items?”, so an empty array passes it.
if (!order.quantity) treats a quantity of 0, entered on purpose, exactly like a missing one. Write the question you
mean instead: items.length > 0, quantity !== undefined, note.trim() !== ''. The short form is fine when every
falsy value really means “nothing here”, for example an optional name that is either a non-empty string or
undefined.
Several cases with else if
Each else if adds one more condition to the chain. JavaScript checks the conditions in the order they are written:
the first one that holds wins, its block runs, and the conditions after it are never evaluated. When none of them
holds, control goes to the final else. Here a shop charges for delivery by distance, in three bands with made-up
rates:
// A delivery fee by distance in kilometres, with made-up rates. Exactly one block of the chain runs.
function deliveryFee(km) {
let fee;
if (km <= 3) {
fee = 25;
} else if (km <= 8) {
fee = 40;
} else if (km <= 15) {
fee = 70;
} else {
fee = null; // too far: no delivery
}
return fee;
}
// Each limit, and a distance just past it.
console.log('1 km:', deliveryFee(1));
console.log('3 km:', deliveryFee(3));
console.log('3.1 km:', deliveryFee(3.1));
console.log('8 km:', deliveryFee(8));
console.log('8.5 km:', deliveryFee(8.5));
console.log('15 km:', deliveryFee(15));
console.log('15.2 km:', deliveryFee(15.2)); Output
1 km: 25 3 km: 25 3.1 km: 40 8 km: 40 8.5 km: 70 15 km: 70 15.2 km: null
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node delivery-fee.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
How the else if chain of delivery-fee.mjs picks exactly one block
Text description of the diagram
A flowchart of the chain in deliveryFee(km), read from the top.
- The first test is km <= 3. When it is true, fee becomes 25, and no other test runs.
- When it is false, the second test, km <= 8, runs. True sets fee to 40. It needs no lower limit, because only distances above 3 ever reach it.
- When that is false too, km <= 15 runs. True sets fee to 70.
- When all three tests are false, the else block sets fee to null: the distance is too far for a delivery.
Every path ends after exactly one block has run, and the function then returns fee, so every distance gets exactly one fee.
Read the chain the way JavaScript does. For 8.5 km, km <= 3 and km <= 8 are both false, so km <= 15 decides and
the fee is 70. The second test needs no lower limit: it only ever runs after km <= 3 was false, so every distance
that reaches it is already above 3. The program tries each limit and a value just past it, 3 and 3.1, 8 and 8.5, 15
and 15.2, because that is where chains go wrong: a < typed where <= was meant changes the answer for the limit
itself and for nothing else. And because the chain ends with else, every distance gets an answer, even one that no
band covers.
A common mistake: the widest test first
Swapping the tests around changes the answers. Here the widest limit comes first:
// The same three limits with the widest test first: it is true for every distance up to 15 km.
function deliveryFee(km) {
let fee;
if (km <= 15) {
fee = 70;
} else if (km <= 8) {
fee = 40; // can never run: a distance of 8 km or less already matched km <= 15
} else if (km <= 3) {
fee = 25; // can never run either
} else {
fee = null;
}
return fee;
}
console.log('1 km:', deliveryFee(1));
console.log('8 km:', deliveryFee(8));
console.log('16 km:', deliveryFee(16)); Output
1 km: 70 8 km: 70 16 km: null
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node wrong-order.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
A 1 km delivery now costs 70. km <= 15 is true for it, so the first block runs and the cheaper bands are never
reached. There is no error and no warning: the two else if blocks have simply become code that can never run, and
the linter’s rules do not notice. Put the smallest band first and work outwards, or write each band with both of its
ends (km > 3 && km <= 8) so that the order stops mattering; either way, test every limit.
Two conditions that ignore the data
Some conditions look right but give the same answer whatever the data. Both of these are common in real code, and both are easy to catch:
// Bug 1: = assigns and === compares. The assignment's value, 0, becomes the condition.
let attempts = 3;
if (attempts = 0) {
console.log('Locked out');
} else {
console.log(`attempts is now ${attempts}: the test changed it`);
}
// Bug 2: each side of || is tested on its own, and a non-empty string counts as true.
const status = 'cancelled';
if (status === 'paid' || 'refunded') {
console.log(`Treated as settled: ${status}`);
}
// The fixes: compare the variable each time, or ask whether a list includes it.
if (status === 'paid' || status === 'refunded') console.log('not printed');
if (['paid', 'refunded'].includes(status)) console.log('not printed either');
console.log('end of program'); Output
attempts is now 0: the test changed it Treated as settled: cancelled end of program
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node condition-bugs.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
In the first test, attempts = 0 is an assignment, not a comparison. It stores 0 in attempts, and its value, 0,
becomes the condition, so the else block runs and shows that the test itself changed the variable. The answer
depends only on the value assigned: attempts = 5 would make the condition 5, which counts as true, and lock everyone
out. Compare with ===, which checks whether two values are the same without converting either of them; the lesson on
equality explains why code avoids ==.
In the second, status === 'paid' || 'refunded' means (status === 'paid') || 'refunded', because === binds more
tightly than ||. The comparison is false, so || gives back its right-hand side, the string 'refunded', which is
not empty and therefore counts as true: every order is treated as settled. Write the comparison out each time,
status === 'paid' || status === 'refunded', or ask whether a list contains the value,
['paid', 'refunded'].includes(status), which stays short as the list grows.
ESLint’s recommended rules report both mistakes. no-cond-assign flags the assignment with “Expected a conditional
expression and instead saw an assignment”, and no-constant-condition flags both conditions with “Unexpected constant
condition”. The wrong-order chain above, by contrast, passes every rule, which is why the boundary values belong in
your tests.
Choosing a value with the ternary operator
condition ? valueIfTrue : valueIfFalse is an expression, so it can stand wherever a value can: inside a template
literal, after return, on the right of = or as an argument. JavaScript evaluates the condition, then only the side
it chose:
// condition ? valueIfTrue : valueIfFalse is an expression: it produces a value you can store or print.
const count = 1;
console.log(`${count} ${count === 1 ? 'item' : 'items'} in the basket`);
// Only the side that is chosen runs, so the other side may hold code that would fail.
const user = null;
const name = user !== null ? user.name : 'guest'; // user.name is never read
console.log(`Hello, ${name}`);
// A chain of ternaries reads like an else if chain when each test starts a new line.
function grade(marks) {
return marks >= 90 ? 'A'
: marks >= 75 ? 'B'
: marks >= 50 ? 'C'
: 'F';
}
console.log(grade(93), grade(75), grade(74), grade(12)); Output
1 item in the basket Hello, guest A B C F
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node ternary.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
Reading user.name would throw a TypeError while user is null, but it never happens, because the condition is
false and that side is not evaluated. The third operand may itself be a ternary: a ? x : b ? y : z groups as
a ? x : (b ? y : z), so the tests still run in order from left to right, and a chain laid out one test per line, as
in grade, reads like an else if chain. Keep ternaries for choosing between values. When each branch has to do
something, when there are more than three cases, or when one ternary would end up inside another’s middle operand, an
if statement says it more clearly. Teams that want no chains at all turn on ESLint’s no-nested-ternary rule.
Flatter code with guard clauses
Every condition you nest pushes the real work further to the right and further away from the test it depends on. The two versions below apply the same three rules for a discount coupon and print identical results, but they read very differently:
Nested · coupon-nested.mjs
// Each rule adds a level of nesting, and every else sits far below the test it answers.
function couponProblem(cart, coupon) {
if (cart.items.length > 0) {
if (!coupon.expired) {
if (cart.total >= coupon.minTotal) {
return null; // no problem: the coupon applies
} else {
return `add ${coupon.minTotal - cart.total} more to use ${coupon.code}`;
}
} else {
return `${coupon.code} has expired`;
}
} else {
return 'the cart is empty';
}
}
const save50 = { code: 'SAVE50', minTotal: 500, expired: false };
const welcome = { code: 'WELCOME', minTotal: 300, expired: true };
console.log(couponProblem({ items: [], total: 0 }, save50) ?? 'coupon applies');
console.log(couponProblem({ items: ['lamp'], total: 450 }, save50) ?? 'coupon applies');
console.log(couponProblem({ items: ['lamp', 'rug'], total: 1200 }, save50) ?? 'coupon applies');
console.log(couponProblem({ items: ['rug'], total: 750 }, welcome) ?? 'coupon applies'); Output
the cart is empty add 50 more to use SAVE50 coupon applies WELCOME has expired
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node coupon-nested.mjs
Guard clauses · coupon-guards.mjs
// The same rules as guard clauses: each problem is checked and returned first; the normal case comes last.
function couponProblem(cart, coupon) {
if (cart.items.length === 0) return 'the cart is empty';
if (coupon.expired) return `${coupon.code} has expired`;
if (cart.total < coupon.minTotal) return `add ${coupon.minTotal - cart.total} more to use ${coupon.code}`;
return null; // no problem: the coupon applies
}
const save50 = { code: 'SAVE50', minTotal: 500, expired: false };
const welcome = { code: 'WELCOME', minTotal: 300, expired: true };
console.log(couponProblem({ items: [], total: 0 }, save50) ?? 'coupon applies');
console.log(couponProblem({ items: ['lamp'], total: 450 }, save50) ?? 'coupon applies');
console.log(couponProblem({ items: ['lamp', 'rug'], total: 1200 }, save50) ?? 'coupon applies');
console.log(couponProblem({ items: ['rug'], total: 750 }, welcome) ?? 'coupon applies'); Output
the cart is empty add 50 more to use SAVE50 coupon applies WELCOME has expired
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node coupon-guards.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
In the nested version, the message for an empty cart is 11 lines below the test that produces it. The second version
deals with each problem first and returns at once. These early checks are called guard clauses. Because return
leaves the function, a line after a guard runs only when the guard did not, so no else is needed, and the coupon
that passes every check is handled on the last line, at the function’s outer level. Each rule is one line, so a fourth
rule, say a coupon that only new customers may use, adds one line instead of one more level of nesting. The same idea
works with throw for input that a function must refuse, as the exercise shows, and with continue and break
inside loops, which the lesson on loops covers.
When a lookup beats an if chain
A long chain that compares one value with a list of fixed values is really a table written as code. You can store the table as data instead:
// The same fixed mapping twice: letter grades to points on a made-up 10-point scale.
function pointsWithIfs(grade) {
let points;
if (grade === 'A') points = 10;
else if (grade === 'B') points = 8;
else if (grade === 'C') points = 6;
else if (grade === 'D') points = 4;
else if (grade === 'E') points = 2;
else points = 0;
return points;
}
// As data: one entry per grade, and one lookup instead of up to five tests.
const POINTS = new Map([
['A', 10],
['B', 8],
['C', 6],
['D', 4],
['E', 2],
]);
function pointsWithMap(grade) {
return POINTS.get(grade) ?? 0;
}
console.log('B:', pointsWithIfs('B'), pointsWithMap('B'));
console.log('E:', pointsWithIfs('E'), pointsWithMap('E'));
console.log('Z:', pointsWithIfs('Z'), pointsWithMap('Z'));
console.log('grades on the scale:', POINTS.size); Output
B: 8 8 E: 2 2 Z: 0 0 grades on the scale: 5
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node lookup-vs-ifs.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
Both functions agree. The Map holds each grade and its points once, finds an answer with one get call where the
chain may run five tests, can be counted (POINTS.size), listed or loaded from a file, and ?? 0 supplies the answer
for a grade it does not know. The ECMAScript specification requires a Map to find a key faster than checking its
entries one by one: on average, its access time must grow more slowly than the number of entries. A chain still wins
when the tests are ranges, like the delivery bands, or combine several values. The next lesson adds the third way to
choose, the switch statement, and compares all three.
Exercises
Exercise · Easy · JavaScript
Choose the fare category of a passenger
A city's buses charge by fare category. Write getFareCategory(age, isStudent), which returns one of the strings 'child', 'student', 'senior' and 'adult'.
- Passengers under 12 are
'child', students included. - Passengers aged 60 or more are
'senior', students included. - Students aged 12 to 59 are
'student'. - Everyone else is
'adult'. agemust be a whole number of years, 0 or more. For anything else, such as-1,2.5,NaNor the string'30', throw aRangeErrorbefore any other test.isStudentis alwaystrueorfalse.
So getFareCategory(11, true) is 'child', getFareCategory(12, true) is 'student', getFareCategory(12, false) is 'adult' and getFareCategory(60, true) is 'senior'.
Starter code · fares.mjs
/**
* The fare category of a passenger (the rules are in the prompt).
* @param {number} age a whole number of years, 0 or more
* @param {boolean} isStudent
* @returns {'child' | 'student' | 'senior' | 'adult'}
*/
export function getFareCategory(age, isStudent) {
return 'adult';
} The sample tests · fares.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { getFareCategory } from './fares.mjs';
test('passengers under 12 are children', () => {
assert.equal(getFareCategory(0, false), 'child');
assert.equal(getFareCategory(5, false), 'child');
assert.equal(getFareCategory(11, false), 'child');
});
test('students under 12 are still children', () => {
assert.equal(getFareCategory(8, true), 'child');
assert.equal(getFareCategory(11, true), 'child');
});
test('passengers from 12 to 59 who are not students are adults', () => {
assert.equal(getFareCategory(12, false), 'adult');
assert.equal(getFareCategory(30, false), 'adult');
assert.equal(getFareCategory(59, false), 'adult');
});
test('students from 12 to 59 are students', () => {
assert.equal(getFareCategory(12, true), 'student');
assert.equal(getFareCategory(21, true), 'student');
assert.equal(getFareCategory(59, true), 'student');
});
test('passengers of 60 or more are seniors', () => {
assert.equal(getFareCategory(60, false), 'senior');
assert.equal(getFareCategory(84, false), 'senior');
});
test('students of 60 or more are seniors', () => {
assert.equal(getFareCategory(60, true), 'senior');
assert.equal(getFareCategory(67, true), 'senior');
});
test('negative and fractional ages throw a RangeError', () => {
assert.throws(() => getFareCategory(-1, false), RangeError);
assert.throws(() => getFareCategory(2.5, false), RangeError);
});
test('NaN and a string age throw a RangeError', () => {
assert.throws(() => getFareCategory(NaN, true), RangeError);
assert.throws(() => getFareCategory('30', false), RangeError);
}); A hint
Begin with one guard clause that throws for every bad age: Number.isInteger is false for 2.5, NaN and '30' alike. Then return early for the two age rules that ignore isStudent, so that by the time your code looks at isStudent, only passengers aged 12 to 59 are left.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (QuickJS): nothing is sent to mysmartcopilot.com. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Check yourself
5 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
Interview questions
Fresher · What is the difference between an if statement and the conditional (ternary) operator in JavaScript?
An if statement chooses which statements run: each branch of if (condition) { … } else { … } can do anything, such
as assign variables, call functions or return, and the statement itself produces no value. The conditional operator,
condition ? a : b, is an expression: it produces one of two values, so it can be used inside a template literal,
after return or as an argument. Both convert the condition with the truthiness rules, and both run only the branch
they chose, so user ? user.name : 'guest' never reads user.name when user is null. Use the ternary for a short
choice between two values, and an if statement when the branches do work, when there are several cases, or when
ternaries would have to be nested inside each other.
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Fresher to mid
A refund function has five levels of nested if and else blocks, and a bug report says that customers who paid by gift card get the wrong message. How would you restructure the function, and how would you show that the new version gives the same answers as the old one in every other case?
-
Fresher to mid
In a code review you find const fee = isMember ? 0 : total > 500 ? 0 : remote ? 99 : 49; and its tests pass. Would you approve it? Explain how JavaScript groups it, what each kind of customer pays, and what you would ask the author to change.
Key takeaways
- An
ifruns its block only when the condition counts as true, andelseruns the other block. Always write the braces: without them anifcontrols one statement, and anelsebelongs to the nearestifwithout one. - A condition is converted like
Boolean(value): an empty array or object counts as true, 0 and''as false. Test what you mean, such asitems.length > 0. - In an
else ifchain only the first condition that holds runs its block. Put the narrowest band first, finish withelse, and test each limit and the value just past it. x = 0stores a value rather than comparing two, andstatus === 'paid' || 'refunded'holds for every status; ESLint’s recommended rules catch both.condition ? a : bchooses a value and evaluates only the side it chose; useifwhen the branches do things.- Guard clauses check for problems first and return or throw early, so the normal case stays flat; a
Mapcan replace a chain that maps fixed values.
References
- ECMAScript 2026 Language Specification: The if Statement (Ecma International)
- ECMAScript 2026 Language Specification: ToBoolean (Ecma International)
- ECMAScript 2026 Language Specification: Conditional Operator ( ? : ) (Ecma International)
- ECMAScript 2026 Language Specification: Map Objects (Ecma International)
- if...else (MDN Web Docs)
- Conditional (ternary) operator (MDN Web Docs)
- Operator precedence (MDN Web Docs)
- Array.prototype.includes() (MDN Web Docs)
- ESLint rule: no-cond-assign (ESLint (OpenJS Foundation))
- ESLint rule: no-constant-condition (ESLint (OpenJS Foundation))
- ESLint rule: curly (ESLint (OpenJS Foundation))
- ESLint rule: no-nested-ternary (ESLint (OpenJS Foundation))
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress