JavaScript · free · no signup

Learn javascript, with practice after every lesson

10 lessons, about 162 minutes of reading, and 30 multiple-choice questions. Each lesson names the mistake that most often costs people the interview, because that is where the hours actually go. Part of the free coding course.

Variables and the browser

Basics JavaScript · 12 min · 10 XP

JavaScript runs inside every web browser, which makes it the only language where you can see the result of your code on a real page within minutes. That immediate feedback is why many people find it the most motivating first language β€” you change a line, refresh, and something visibly moves.

Use let for a value that will change and const for one that will not. var still works but belongs to older code and behaves in ways that surprise people, so there is no reason to start with it. The distinction that confuses everyone once: const prevents reassignment, not mutation. You cannot point the name at a different array, but you can absolutely push items into the one it already points at.

The equality rule is worth learning on day one rather than discovering later. == converts types before comparing, so "5" == 5 is true, "" == 0 is true, and null == undefined is true. Every one of those has caused a production bug somewhere. Use === unless you have a specific reason not to, and the whole category disappears.

Syntax

const name = "Priya";
let visits = 0;

visits = visits + 1;
console.log(`${name} has ${visits} visit`);

// const protects the binding, not the contents:
const scores = [1, 2];
scores.push(3);        // fine
// scores = [4];       // TypeError

Key points

  • const prevents reassignment, not mutation β€” you can still push to a const array.
  • Template literals use backticks and ${...}, not quotes.
  • console.log is how you inspect values; open DevTools with F12 to see the output.
The mistake that costs people the interview: Using == instead of ===. Loose equality converts types, so "5" == 5 is true. Always use === unless you have a specific reason.

Practice challenge

Add CSS ClassBasics
Task

Use classList.add() to add a class.

Expected output
<div id="card" class="highlight">...</div>
Show a hint
  1. Use .classList.add()
  2. Pass class name as string
  3. No dot before class name

Open this exercise in the app → · Auto-graded coding rounds

Check yourself

1. Which declares a value that will be reassigned?

  1. const
  2. let
  3. final
  4. static
Show answer

B. let

2. What does "5" === 5 evaluate to?

  1. true
  2. false
  3. An error
  4. undefined
Show answer

B. false

3. Can you push to a const array?

  1. Yes β€” const blocks reassignment only
  2. No, never
  3. Only if empty
  4. Only with var
Show answer

A. Yes β€” const blocks reassignment only

Back to the syllabus ↑

Arrays: map, filter, reduce

Working level JavaScript · 18 min · 25 XP

Three array methods cover the overwhelming majority of day-to-day JavaScript. map transforms every item, filter keeps the items matching a condition, and reduce collapses a list into a single value. Chained together they express most data handling you will do in a browser, in a form that reads top to bottom.

Reaching for these instead of a manual for loop is one of the clearest signals in an interview that you have written real JavaScript rather than only studied it. It is not about elegance: map and filter return new arrays and leave the original untouched, which prevents an entire class of bug where a function quietly modifies data its caller still needs.

The detail people miss is reduce's second argument. reduce((sum, x) => sum + x, 0) starts at zero; without that 0, an empty array throws rather than returning zero. Since empty arrays are exactly what you get on a quiet day or a filtered-down dataset, omitting it produces a crash that only appears in the situation you least tested.

Syntax

const orders = [
  { item: "Desk",  price: 8000, paid: true },
  { item: "Chair", price: 4500, paid: false },
  { item: "Lamp",  price: 1200, paid: true }
];

const names   = orders.map(o => o.item);
const unpaid  = orders.filter(o => !o.paid);
const revenue = orders
  .filter(o => o.paid)
  .reduce((sum, o) => sum + o.price, 0);

console.log(names, unpaid.length, revenue);  // 9200

Key points

  • map always returns an array of the same length; filter returns the same or fewer.
  • reduce needs a starting value (the 0 above) or an empty array throws.
  • These methods return new arrays β€” the original is untouched, which prevents a whole class of bug.
The mistake that costs people the interview: Using map when you meant forEach. If you are not using the returned array, map allocates one for nothing β€” say what you mean.

Practice challenge

Check AgeWorking level
Task

Define canDrive(age) that returns true if age>=18, else false. Test with 20.

Expected output
true
Show a hint
  1. Use >= comparison
  2. Return the boolean result
  3. true and false are keywords in JS

Open this exercise in the app → · Auto-graded coding rounds

Check yourself

1. What does revenue evaluate to above?

  1. 13700
  2. 9200
  3. 4500
  4. 0
Show answer

B. 9200

2. Which returns an array of the same length as the input?

  1. filter
  2. reduce
  3. map
  4. find
Show answer

C. map

3. Why pass 0 as reduce's second argument?

  1. Style only
  2. It is the starting value and prevents an empty-array error
  3. It sets the index
  4. It is required by filter
Show answer

B. It is the starting value and prevents an empty-array error

Back to the syllabus ↑

Async: promises and await

Advanced JavaScript · 20 min · 30 XP

Fetching data takes time, and JavaScript does not wait around β€” it starts the request and carries on. A promise represents a value that will arrive later, and await pauses inside an async function until it does. Everything about network code in the browser rests on this.

This is where most self-taught developers get stuck, and it is heavily tested in interviews precisely because misunderstanding it produces code that appears to work and then fails intermittently under real network conditions. The classic version: a page that works on your fast connection and breaks for users on mobile, because a value was read before it had arrived.

Two specifics are worth memorising. fetch does not reject on 404 or 500 β€” it resolves, and you must check res.ok yourself, or you will parse an error page as JSON. And awaiting in a loop runs requests one after another: ten sequential 200ms calls take two seconds, while Promise.all takes 200 milliseconds. Interviewers ask about that difference often.

Syntax

async function loadUser(id) {
  try {
    const res = await fetch(`/api/users/${id}`);
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    const user = await res.json();
    return user;
  } catch (err) {
    console.error("Could not load user:", err.message);
    return null;
  }
}

// Parallel, not one-after-another:
const [a, b] = await Promise.all([loadUser(1), loadUser(2)]);

Key points

  • await only works inside an async function; the function itself returns a promise.
  • fetch does not reject on 404 or 500 β€” you must check res.ok yourself.
  • Promise.all runs requests in parallel; awaiting them one by one is needlessly slow.
The mistake that costs people the interview: Awaiting in a loop when the calls are independent. Ten sequential 200ms calls take two seconds; Promise.all takes 200ms.

Practice challenge

Count Letter OccurrencesAdvanced
Task

Split word into letters, loop through and count "a".

Expected output
3
Show a hint
  1. split("") breaks word into letter array
  2. Count when letter equals "a"
  3. Increment count each time

Open this exercise in the app → · Auto-graded coding rounds

Check yourself

1. Does fetch throw an error on an HTTP 404?

  1. Yes, always
  2. No β€” you must check res.ok
  3. Only in Node
  4. Only with await
Show answer

B. No β€” you must check res.ok

2. What runs requests in parallel?

  1. await in a for loop
  2. Promise.all
  3. forEach
  4. map alone
Show answer

B. Promise.all

3. Where can you use await?

  1. Anywhere
  2. Inside an async function
  3. Only at the top level
  4. Only in try blocks
Show answer

B. Inside an async function

Back to the syllabus ↑

Functions, scope and arrow syntax

Basics JavaScript · 14 min · 15 XP

A function packages work you want to repeat. Modern JavaScript uses arrow functions for short ones, which are both briefer and better behaved inside array methods and callbacks, where the older function keyword brings its own value of `this` and causes confusion.

Scope decides where a name is visible. A variable declared with let or const inside braces exists only inside those braces, which prevents a whole class of accidental overwrites. var ignores block scope entirely and leaks out of if statements and loops, which is the main reason it is avoided in new code.

The arrow-function detail that catches people is the implicit return. (x) => x * 2 returns double x; (x) => { x * 2 } returns undefined, because adding braces turns it into a normal function body where you must write return yourself. The two forms look nearly identical and behave completely differently, and it is worth training your eye to spot the braces.

Syntax

function greet(name) {
  return `Hello, ${name}`;
}

const greetShort = (name) => `Hello, ${name}`;
const add = (a, b = 0) => a + b;

if (true) {
  const inner = "only in here";
}
// console.log(inner);  // ReferenceError

console.log(add(2), add(2, 3));   // 2  5

Key points

  • An arrow function with no braces returns its expression automatically.
  • Default parameters (b = 0) let one function cover several call shapes.
  • let and const are block-scoped; var leaks out of the block, which is why it is avoided.
The mistake that costs people the interview: Adding braces to an arrow function and forgetting return: (x) => { x * 2 } returns undefined, not double x.

Practice challenge

Add Two Numbers FunctionBasics
Task

Define add(a, b) that returns a+b, call it with 10 and 20, and print result.

Expected output
30
Show a hint
  1. Use return keyword
  2. Return a + b
  3. The console.log will print the returned value

Open this exercise in the app → · Auto-graded coding rounds

Check yourself

1. What does (x) => { x * 2 } return?

  1. Double x
  2. undefined
  3. An error
  4. x
Show answer

B. undefined

2. Which is block-scoped?

  1. var
  2. let
  3. function
  4. global
Show answer

B. let

3. What does add(2) return given (a, b = 0) => a + b?

  1. 2
  2. 0
  3. NaN
  4. An error
Show answer

A. 2

Back to the syllabus ↑

Objects and destructuring

Working level JavaScript · 15 min · 20 XP

An object groups related values under names. Almost everything you receive from an API is an object, so reading from them fluently is unavoidable in real work β€” and doing it defensively is what separates code that survives a missing field from code that throws on the first unusual record.

Destructuring pulls fields out into variables in one line, and optional chaining (?.) lets you read a nested field that might not exist without the whole expression throwing. user.company?.name gives undefined rather than a TypeError when company is absent, which is the difference between a blank space on the page and a blank page.

The default-value trap is worth knowing precisely. || falls back whenever the left side is falsy, which includes 0 and the empty string β€” so count || 10 turns a genuine zero into ten. ?? only falls back on null and undefined, which is almost always what you actually meant. Wherever zero is a valid value, || is a bug waiting for the right data.

Syntax

const user = { name: "Priya", role: "Analyst", address: { city: "Pune" } };

const { name, role } = user;
const { city } = user.address;
console.log(name, role, city);

console.log(user.company?.name);        // undefined, not a crash
console.log(user.company?.name ?? "β€”"); // "β€”"

const updated = { ...user, role: "Senior Analyst" };

Key points

  • ?. stops evaluation and yields undefined instead of throwing on a missing parent.
  • ?? supplies a fallback only for null/undefined β€” unlike ||, it keeps 0 and "".
  • { ...user, role: x } copies then overrides, leaving the original untouched.
The mistake that costs people the interview: Using || for defaults on numbers: count || 10 turns a real 0 into 10. Use ?? when zero or empty string are valid values.

Practice challenge

Cricket Runs PatternWorking level
Task

Print: 1, 2, 3, 4, 5, 6 (runs per over)

Expected output
1 2 3 4 5 6
Show a hint
  1. 6 overs total
  2. Each over number equals runs
  3. Use a simple counting loop

Open this exercise in the app → · Auto-graded coding rounds

Check yourself

1. What does user.company?.name give when company is absent?

  1. An error
  2. undefined
  3. null
  4. ""
Show answer

B. undefined

2. Which preserves a valid 0 when defaulting?

  1. ||
  2. ??
  3. &&
  4. !
Show answer

B. ??

3. What does { ...user, role: "X" } do?

  1. Mutates user
  2. Copies user, overriding role
  3. Deletes role
  4. Throws
Show answer

B. Copies user, overriding role

Back to the syllabus ↑

Changing the page (DOM)

Working level JavaScript · 16 min · 25 XP

The DOM is the browser's live model of the page. JavaScript reads and changes it, which is how every interactive site works β€” and being able to demonstrate this is what makes a portfolio project feel real rather than a static mock-up.

The pattern is always the same: select an element, then either change it or listen for an event on it. querySelector takes any CSS selector, so the selectors you already know from styling work here unchanged, which is why CSS and DOM work reinforce each other.

Two safety habits matter more than any technique. Prefer textContent over innerHTML for anything a user typed, because innerHTML executes markup and is the most common route to a cross-site scripting hole in beginner code. And run your script after the elements exist β€” at the end of body, or with defer β€” or querySelector returns null and you get 'Cannot read properties of null', which is the single most-asked beginner question about JavaScript.

Syntax

const btn = document.querySelector("#save");
const out = document.querySelector("#status");

btn.addEventListener("click", (e) => {
  e.preventDefault();
  out.textContent = "Saved at " + new Date().toLocaleTimeString();
  out.classList.add("ok");
});

document.querySelectorAll(".row").forEach((row, i) => {
  row.textContent = `Row ${i + 1}`;
});

Key points

  • querySelector takes any CSS selector: #id, .class, tag.
  • Prefer textContent over innerHTML β€” innerHTML with user input is an XSS hole.
  • e.preventDefault() stops a form submitting and reloading the page.
The mistake that costs people the interview: Running the script before the element exists. Put the script at the end of body or use defer, or querySelector returns null and you get 'Cannot read properties of null'.

Practice challenge

Even Numbers GameWorking level
Task

Loop from 1 to 10 and only print even numbers.

Expected output
2 4 6 8 10
Show a hint
  1. A number is even if it divides by 2 with no remainder
  2. Use i % 2 === 0
  3. The % operator gives remainder

Open this exercise in the app → · Auto-graded coding rounds

Check yourself

1. Which is safer for inserting user text?

  1. innerHTML
  2. textContent
  3. outerHTML
  4. insertAdjacentHTML
Show answer

B. textContent

2. Why does querySelector return null?

  1. The element does not exist yet or the selector is wrong
  2. The page is cached
  3. CSS is missing
  4. It never does
Show answer

A. The element does not exist yet or the selector is wrong

3. What stops a form reloading the page?

  1. e.stop()
  2. e.preventDefault()
  3. return true
  4. e.cancel()
Show answer

B. e.preventDefault()

Back to the syllabus ↑

Coercion, ===, and the comparisons that lie

Working level JavaScript · 15 min · 15 XP

JavaScript converts types automatically when you compare or combine values, and that convenience produces its most notorious results. == converts both sides before comparing, so '5' == 5 is true, and so are a handful of pairs nobody wants: 0 == '', null == undefined, and '0' == false. === compares without converting and is what you should use everywhere; the rule is simple enough that 'always ===' costs nothing and removes a whole category of bug.

The + operator is the other half, because it means both addition and string concatenation and decides by looking at its operands. If either side is a string, both become strings, which is why '5' + 3 is '53' while '5' - 3 is 2 β€” minus has no string meaning so both sides become numbers. Values arriving from a form input, a URL parameter or JSON are strings, so a total computed with + silently concatenates and produces something that looks almost right, like '1500' from 15 and 00.

Two more are worth committing to memory. NaN is not equal to itself, so x === NaN is always false and Number.isNaN(x) is the only reliable check. And numbers are floating point, so 0.1 + 0.2 is 0.30000000000000004 β€” not a JavaScript flaw but the binary representation every language shares, which is why money should be held in the smallest unit as an integer, in paise or cents, rather than as a decimal you round at the end and hope.

Syntax

// == converts, === does not. Always ===.
'5' == 5          // true
'5' === 5         // false
0 == ''           // true
0 == '0'          // true
'' == '0'         // false   <- not even transitive
null == undefined // true
null === undefined// false

// + means addition OR concatenation, decided by the operands
'5' + 3           // '53'   -- either side a string: both become strings
'5' - 3           // 2      -- minus has no string meaning
[] + {}           // '[object Object]'

// input is ALWAYS a string
const qty = document.querySelector('#qty').value;   // '15'
const total = qty + '00';                           // '1500'  looks plausible
const total2 = Number(qty) * 100;                   // 1500     correct

// parseInt vs Number
Number('12abc')     // NaN      -- strict
parseInt('12abc')   // 12       -- stops at the first non-digit
Number('')          // 0        <- empty string is NOT NaN

// NaN is not equal to itself
NaN === NaN         // false
Number.isNaN(x)     // the only reliable check

// floating point -- true in every language
0.1 + 0.2           // 0.30000000000000004
0.1 + 0.2 === 0.3   // false
// money: hold the smallest unit as an integer
const paise = 1050;                 // not 10.50
(paise / 100).toFixed(2);           // '10.50' for display only

// falsy values, exactly: false 0 -0 0n '' null undefined NaN
if (count) { }              // skipped when count is 0 -- often a bug
if (count !== undefined) { }

Key points

  • Use === everywhere. == converts before comparing and produces 0 == '', '0' == false and a comparison that is not even transitive.
  • + concatenates if either operand is a string, and every value from an input, URL or JSON is a string. Convert with Number() before arithmetic.
  • Money in floating point accumulates error. Hold the smallest unit as an integer and format only for display.
The mistake that costs people the interview: Treating a form value as a number because it looks like one. Every input value is a string, so + concatenates instead of adding, and the result β€” '1500' from 15 and '00' β€” is plausible enough to reach production before anyone notices the total is wrong.

Practice challenge

Event Listener ClickWorking level
Task

Use addEventListener to handle click event.

Expected output
Button clicked!
Show a hint
  1. Use addEventListener()
  2. First param is "click"
  3. Second param is callback function

Open this exercise in the app → · Auto-graded coding rounds

Check yourself

1. What is '5' + 3?

  1. 8
  2. '53' β€” either operand a string makes both strings
  3. NaN
  4. TypeError
Show answer

B. '53' β€” either operand a string makes both strings

2. How do you reliably test whether x is NaN?

  1. x === NaN
  2. Number.isNaN(x)
  3. x == NaN
  4. typeof x === 'NaN'
Show answer

B. Number.isNaN(x)

3. Why store money as paise or cents in an integer?

  1. It is faster
  2. Floating point cannot represent decimal fractions exactly, so errors accumulate
  3. Integers sort better
  4. It uses less memory
Show answer

B. Floating point cannot represent decimal fractions exactly, so errors accumulate

Back to the syllabus ↑

fetch, JSON and handling failure properly

Advanced JavaScript · 17 min · 20 XP

fetch makes an HTTP request and returns a promise for a Response, and its first surprise is what counts as failure. The promise rejects only on a network-level problem β€” offline, DNS failure, CORS refusal. A 404 or a 500 is a successful round trip as far as fetch is concerned, so the promise resolves and your catch block never runs. You must check response.ok yourself, and code that does not will happily parse an error page as data and fail somewhere further downstream with a confusing message.

Reading the body is a second async step: response.json() itself returns a promise, which is why the pattern is two awaits. The body can also be read only once, so calling .json() after .text() on the same response throws. When an error response carries a JSON body explaining the problem, read it before throwing so your error message contains the server's reason rather than a bare status code β€” that difference is what makes a production log useful.

For real use, three things belong around every request. A timeout, because fetch has none by default and a hung request will wait indefinitely β€” AbortController with a timer provides it. A distinction in your catch between a network failure, an HTTP error and a JSON parse failure, since they need different responses. And never put a secret in the request from browser code: anything the browser can send, a user can read in the network tab, so an API key in client-side fetch is a published key regardless of how the code is bundled.

Syntax

async function loadOrders(customerId) {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), 8000);   // fetch has NO default timeout

  try {
    const res = await fetch(`/api/customers/${customerId}/orders`, {
      headers: { 'Accept': 'application/json' },
      signal: controller.signal,
    });

    // A 404 or 500 DOES NOT reject. You must check.
    if (!res.ok) {
      let detail = '';
      try { detail = (await res.json()).message ?? ''; } catch { /* not JSON */ }
      throw new Error(`HTTP ${res.status} ${res.statusText}${detail ? ': ' + detail : ''}`);
    }

    return await res.json();          // reading the body is a SECOND await
  } catch (err) {
    if (err.name === 'AbortError') throw new Error('Request timed out');
    throw err;                        // network vs HTTP vs parse: different responses
  } finally {
    clearTimeout(timer);
  }
}

// POST with a JSON body
await fetch('/api/orders', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ items, total }),      // must be a string
});

// The body can be read ONCE
const res = await fetch(url);
await res.text();
await res.json();          // TypeError: body already read

// NEVER put a secret in browser code. Anything the browser sends,
// a user can read in the network tab. Proxy it through your server.

// parallel, not sequential
const [a, b] = await Promise.all([fetch(u1), fetch(u2)]);

Key points

  • fetch rejects only on network failure. A 404 or 500 resolves normally, so check response.ok or you will parse an error page as data.
  • Reading the body is a second promise, and it can be read only once. Calling .json() after .text() on the same response throws.
  • fetch has no default timeout. Use AbortController with a timer, and never send a secret from browser code β€” the network tab shows everything.
The mistake that costs people the interview: Wrapping fetch in try/catch and assuming the catch handles server errors. It does not: a 500 resolves successfully, .json() parses the error page or throws a syntax error, and the failure surfaces later as a confusing message far from the request that actually failed.

Practice challenge

Data ValidationAdvanced
Task

Check if email contains @ and . with correct pattern.

Expected output
true
Show a hint
  1. Email must have @ symbol
  2. Email must have . after @
  3. Use includes() or regex

Open this exercise in the app → · Auto-graded coding rounds

Check yourself

1. Does fetch reject on a 404?

  1. Yes
  2. No β€” it resolves; you must check response.ok
  3. Only for GET
  4. Only with a JSON body
Show answer

B. No β€” it resolves; you must check response.ok

2. What happens if you call res.json() after res.text()?

  1. It returns the parsed body
  2. It throws β€” the body can only be read once
  3. It returns null
  4. It re-fetches
Show answer

B. It throws β€” the body can only be read once

3. How do you time out a fetch?

  1. The timeout option
  2. AbortController with a timer β€” fetch has no default timeout
  3. Promise.race is impossible
  4. It times out after 30s automatically
Show answer

B. AbortController with a timer β€” fetch has no default timeout

Back to the syllabus ↑

Closures, `this`, and the callback that loses itself

Advanced JavaScript · 17 min · 20 XP

A closure is a function that remembers the variables where it was defined, even after that scope has returned. It is not an advanced feature you occasionally reach for; it is the mechanism behind every callback that uses an outer variable, every event handler, and every function returning a configured function. Once you see that a function carries its birthplace with it, a whole class of behaviour stops being mysterious β€” including why a counter created by a factory keeps its own private count that nothing outside can reach.

The classic demonstration is a loop creating functions. With var, there is one shared binding for the whole loop, so every function created inside it sees the final value and three handlers all report 3. With let, each iteration gets a fresh binding, and each function closes over its own β€” which is the single most useful practical reason to have stopped using var. The same shape appears whenever handlers are created in a loop over rows and every one of them acts on the last row.

`this` is a separate mechanism that people merge with closures, and the difference is exactly this: `this` in a regular function is decided by how the function is called, not where it was written. Pass an object's method as a callback and it is called plainly, so `this` is undefined and the method fails on a field that clearly exists. An arrow function has no `this` of its own and takes it from the surrounding scope at definition β€” which is why arrows fixed the callback problem, and equally why an arrow is the wrong choice for an object method that needs `this` to be the object.

Syntax

// A function remembers where it was defined
function makeCounter() {
  let count = 0;                       // private; nothing outside can reach it
  return () => ++count;
}
const next = makeCounter();
next(); next();                        // 1, 2

// THE LOOP CLASSIC
for (var i = 0; i < 3; i++) setTimeout(() => console.log(i), 0);
// 3, 3, 3  -- one shared binding, all three see the final value

for (let i = 0; i < 3; i++) setTimeout(() => console.log(i), 0);
// 0, 1, 2  -- a fresh binding per iteration

// `this` is decided by HOW a function is CALLED
const cart = {
  items: ['a', 'b'],
  count() { return this.items.length; },
};
cart.count();                          // 2   -- called ON cart

const fn = cart.count;
fn();                                  // TypeError: this.items is undefined
setTimeout(cart.count, 100);           // same failure: called plainly

// fixes
setTimeout(() => cart.count(), 100);   // arrow keeps the surrounding `this`
setTimeout(cart.count.bind(cart), 100);

// ...but an arrow is WRONG for a method that needs `this` to be the object
const broken = {
  items: ['a'],
  count: () => this.items.length,      // `this` is the outer scope, not broken
};

// The React/DOM shape of the same bug
class Row {
  constructor(n) { this.n = n; }
  handle() { console.log(this.n); }
}
btn.addEventListener('click', row.handle);          // `this` is the button
btn.addEventListener('click', () => row.handle());  // correct

Key points

  • A closure captures variables, not values. var gives one binding for a whole loop so every function sees the final value; let gives a fresh binding per iteration.
  • `this` in a regular function depends on how it is called. Passing a method as a callback detaches it, and `this` becomes undefined.
  • An arrow function takes `this` from the surrounding scope, which fixes callbacks and breaks object methods. The right choice depends on which you need.
The mistake that costs people the interview: Passing an object's method directly as an event handler or to setTimeout. It is then called plainly rather than on the object, so `this` is not the object and the method fails on a property that visibly exists β€” which reads as the object being wrong rather than the call site.

Practice challenge

DOM List CreationAdvanced
Task

Loop through array and create list items.

Expected output
<li>Anita</li><li>Bhavna</li><li>Chitra</li>
Show a hint
  1. createElement("li") creates list item
  2. forEach loops through array
  3. appendChild adds to parent

Open this exercise in the app → · Auto-graded coding rounds

Check yourself

1. Why do three setTimeout callbacks in a var loop all print 3?

  1. setTimeout batches them
  2. var creates one shared binding, so all closures see the final value
  3. The loop runs after the timeouts
  4. 3 is the array length
Show answer

B. var creates one shared binding, so all closures see the final value

2. You pass cart.count to setTimeout and it throws. Why?

  1. setTimeout cannot take methods
  2. It is called plainly, so `this` is not cart
  3. The method is private
  4. count is not a function
Show answer

B. It is called plainly, so `this` is not cart

3. When is an arrow function the wrong choice?

  1. In a callback
  2. As an object method that needs `this` to be the object
  3. Inside a loop
  4. As an event listener
Show answer

B. As an object method that needs `this` to be the object

Back to the syllabus ↑

Modules, npm and shipping a real project

Job-ready JavaScript · 18 min · 25 XP

Modern JavaScript is written in modules: each file has its own scope, states what it exports, and states what it imports. That replaces the era of scripts sharing one global namespace, where load order mattered and any file could overwrite another's variable. Two systems coexist β€” ES modules with import/export, which is the standard and what to write today, and CommonJS with require/module.exports, which Node used first and which you will still meet constantly in older code and tutorials.

npm is where dependencies come from, and two files describe the state of your project. package.json lists what you asked for, usually as a range like ^4.17.0 meaning 'this version or any compatible newer one'. package-lock.json records exactly what was installed, down to every transitive dependency. The lock file is what makes a build reproducible, so it belongs in version control β€” and the single most common cause of 'works locally, fails in CI' is a lock file that was not committed, letting CI resolve different versions from the same range.

Adding a dependency is a decision with a cost, not a free action. Each one brings its own dependencies, its own vulnerabilities and its own maintenance risk, so a package that saves ten lines is rarely worth it while one that saves a thousand usually is. Beyond that, the habits that make a project look professional are small: a .gitignore that excludes node_modules and .env from the very first commit, dependencies split from devDependencies so a production install does not pull your test framework, a README stating how to run it, and npm audit run before shipping rather than after.

Syntax

// ES modules -- the standard. Each file has its own scope.
// money.js
export function toPaise(rupees) { return Math.round(rupees * 100); }
export const GST = 0.18;
export default class Invoice { }

// main.js
import Invoice, { toPaise, GST } from './money.js';   // note the extension
import * as money from './money.js';

// CommonJS -- Node's original, still everywhere in older code
const { toPaise } = require('./money.js');
module.exports = { toPaise };

// package.json -- what you ASKED for
{
  "type": "module",
  "scripts": { "start": "node main.js", "test": "node --test" },
  "dependencies":    { "date-fns": "^4.1.0" },   // ^ = compatible newer allowed
  "devDependencies": { "vitest": "^2.0.0" }      // not installed in production
}

// package-lock.json -- what was ACTUALLY installed, exactly,
// including every transitive dependency. COMMIT IT.
// Not committing it is the usual cause of "works locally, fails in CI":
// CI resolves different versions from the same range.

npm ci        // install exactly the lock file -- what CI should run
npm install   // may update the lock file
npm audit     // before shipping, not after

// .gitignore, from the FIRST commit
node_modules/
.env
dist/

// Every dependency carries its own dependencies, vulnerabilities and
// maintenance risk. Saves 10 lines: rarely worth it. Saves 1,000: usually is.

Key points

  • Modules give each file its own scope and explicit imports, replacing load-order-dependent globals. Write ES modules; expect to read CommonJS.
  • package.json records the range you asked for; package-lock.json records exactly what was installed. Commit the lock file β€” it is what makes CI match your machine.
  • Every dependency brings its own tree, vulnerabilities and maintenance burden. Judge it against how much it actually saves.
The mistake that costs people the interview: Leaving package-lock.json out of version control, or gitignoring it because it is large and noisy in diffs. Every machine then resolves its own versions from the same ranges, and the resulting bug appears only in CI or only in production, with identical source code on both sides.

Practice challenge

Inventory Item CountAdvanced
Task

Count occurrences of "potion" in array.

Expected output
2
Show a hint
  1. Loop through array
  2. Count when item === "potion"
  3. Two potions in the list

Open this exercise in the app → · Auto-graded coding rounds

Check yourself

1. What does package-lock.json record?

  1. The ranges you asked for
  2. Exactly what was installed, including transitive dependencies
  3. Only direct dependencies
  4. Your npm account
Show answer

B. Exactly what was installed, including transitive dependencies

2. Which command installs exactly the lock file, for CI?

  1. npm install
  2. npm ci
  3. npm update
  4. npm audit
Show answer

B. npm ci

3. What does "^4.1.0" mean in package.json?

  1. Exactly 4.1.0
  2. That version or any compatible newer one
  3. At most 4.1.0
  4. Any version at all
Show answer

B. That version or any compatible newer one

Back to the syllabus ↑

Common questions

Do I need any background to start JavaScript?

No. This track begins at its own beginning and assumes nothing, and the first lesson explains what the thing is before showing you any syntax.

How long does the JavaScript track take?

About 162 minutes of reading across 10 lessons, plus the practice challenges and 30 multiple-choice questions, which is where the time actually goes.

Is it free?

Yes, and there is no account. Everything runs in your browser.

More: all 15 tracks · what employers actually ask for · the full syllabus

Keep reading

The STAR method, properly: how to build answers that hold up
A working guide to STAR interview answers: how to weight each part, how to build five stories that cover most…
Returning to work after a career break: rebuilding confidence and explaining the gap
How to present a career break on your CV, close the confidence gap, and answer interview questions about time…
Free AI interview coach
Free AI interview coach: voice mock interviews that talk back, role-specific questions, coding practice and…
Interview countdown, prediction & mock practice
Free interview prep: a live countdown to your interview date, then the 15 most common questions as flip-cards…