# JavaScript Mastery Lab — 50 lessons

DiscoveryVIP · October 9, 2026

Use solve(input) as each exercise entry point. The browser runs language exercises in a time-limited worker. DOM access, imports and network calls are outside this runner. Checks cover the displayed examples, not every possible input.

## 1. Your first returned value

JavaScript evaluates expressions to produce values. A function packages instructions that can be called later. The return statement sends a value back to its caller; console.log only displays a message. In this course the runner calls solve(input) with several examples.

**Challenge:** Return the string Hello, JavaScript!

**Hint:** Put the exact greeting between quotes after return.

```javascript
function solve(input) { return "Hello, JavaScript!"; }
```

**Go further:** Change one test input in your own experiments. Explain which part of your code determines the result.

## 2. Name a value with const

A variable gives a value a useful name. const prevents assigning another value to that binding. It does not make an object deeply immutable. Use names that explain what the value represents rather than how it is stored.

**Challenge:** Store input in a const named name, then return name.

**Hint:** Declare const name = input; then return it.

```javascript
function solve(input) { const name = input; return name; }
```

**Go further:** Change one test input in your own experiments. Explain which part of your code determines the result.

## 3. Update a value with let

Use let when you need to reassign a binding. Assignment updates the value; it is different from a comparison. A variable must be declared before you use it in this block. Prefer a small scope so the changes are easy to follow.

**Challenge:** Start a score at input, add 10, return it.

**Hint:** Use += to add to a let binding.

```javascript
function solve(input) { let score = input; score += 10; return score; }
```

**Go further:** Change one test input in your own experiments. Explain which part of your code determines the result.

## 4. Calculate a total

Arithmetic operators work with numeric values. Parentheses make the intended grouping explicit. This exercise uses whole-number prices to avoid floating-point currency rounding questions. In a real money workflow, define how rounding works.

**Challenge:** Return price multiplied by quantity from input.

**Hint:** Read object fields with input.price and input.quantity.

```javascript
function solve(input) { return input.price * input.quantity; }
```

**Go further:** Change one test input in your own experiments. Explain which part of your code determines the result.

## 5. Build a template literal

Template literals use backticks and ${expression} to insert values. They help keep readable text separate from calculation. A quoted string using normal quotes does not interpret ${...}. Exact spaces and punctuation matter when a program checks text.

**Challenge:** Return Hello, NAME! using input as the name.

**Hint:** Use backticks around the template.

```javascript
function solve(input) { return `Hello, ${input}!`; }
```

**Go further:** Change one test input in your own experiments. Explain which part of your code determines the result.

## 6. Inspect a type

Values have types; variables can hold different types over time. typeof is useful for primitives, but typeof null is the historical string object. Arrays are also objects, so use Array.isArray when you need to distinguish them.

**Challenge:** Return typeof input.

**Hint:** typeof is an operator, not a string property.

```javascript
function solve(input) { return typeof input; }
```

**Go further:** Change one test input in your own experiments. Explain which part of your code determines the result.

## 7. Convert numeric text

Number converts an entire value to a number. Invalid numeric text becomes NaN. Conversion should be intentional: the plus operator can concatenate strings. Empty strings convert to zero, which may need separate validation in real forms.

**Challenge:** Convert input to a number and add 5.

**Hint:** Convert first, add second.

```javascript
function solve(input) { return Number(input) + 5; }
```

**Go further:** Change one test input in your own experiments. Explain which part of your code determines the result.

## 8. Compare without coercion

Strict equality checks equality without converting types first. A number and a string containing digits remain different values. This avoids surprising matches in validation. For objects, strict equality compares identity, not their field contents.

**Challenge:** Return whether input.a strictly equals input.b.

**Hint:** Use three equals signs.

```javascript
function solve(input) { return input.a === input.b; }
```

**Go further:** Change one test input in your own experiments. Explain which part of your code determines the result.

## 9. Combine conditions

Logical AND requires both conditions to be truthy. Logical OR accepts either. These operators return operand values, not necessarily booleans, but comparisons produce booleans. Keep access rules readable with explicit conditions.

**Challenge:** Return true if age is at least 18 and member is true.

**Hint:** Combine the age comparison with the membership comparison using &&.

```javascript
function solve(input) { return input.age >= 18 && input.member === true; }
```

**Go further:** Change one test input in your own experiments. Explain which part of your code determines the result.

## 10. Preserve meaningful zeroes

Nullish coalescing uses a fallback only for null or undefined. Logical OR also replaces values such as 0, false and an empty string. Choose the operator according to your data rules instead of treating every falsy value as missing.

**Challenge:** Return input, or 10 when input is nullish.

**Hint:** Use ?? rather than ||.

```javascript
function solve(input) { return input ?? 10; }
```

**Go further:** Change one test input in your own experiments. Explain which part of your code determines the result.

## 11. Choose with if and else

A conditional selects a path. Return can finish the function early, so an else is not always necessary after a returning branch. Be precise about the boundary: greater than and greater than or equal to express different rules.

**Challenge:** Return adult when input >= 18, otherwise minor.

**Hint:** Test the boundary with >=.

```javascript
function solve(input) { if (input >= 18) return "adult"; return "minor"; }
```

**Go further:** Change one test input in your own experiments. Explain which part of your code determines the result.

## 12. Validate before processing

A guard clause rejects an invalid case before the main work. Number.isFinite accepts only finite numeric values, without converting strings. Early validation reduces nested conditions and makes the assumptions in the rest of a function clearer.

**Challenge:** Return input doubled if it is a finite number; otherwise invalid.

**Hint:** Number.isFinite distinguishes the number 4 from the string 4.

```javascript
function solve(input) { if (!Number.isFinite(input)) return "invalid"; return input * 2; }
```

**Go further:** Change one test input in your own experiments. Explain which part of your code determines the result.

## 13. Use a switch

switch compares a value to case labels with strict equality. A return exits the function; when using statements instead, break avoids falling into the following case. The default path makes unknown values explicit.

**Challenge:** Map red to stop, green to go, and everything else to wait.

**Hint:** Each case can return its result.

```javascript
function solve(input) { switch(input) { case "red": return "stop"; case "green": return "go"; default: return "wait"; } }
```

**Go further:** Change one test input in your own experiments. Explain which part of your code determines the result.

## 14. Normalize user text

Strings are immutable: methods return a new string. trim removes surrounding whitespace; toLowerCase changes case. Chaining is readable when each step has one clear purpose. Normalization should not silently alter data that must retain its original form.

**Challenge:** Trim input and return it in lowercase.

**Hint:** Call trim(), then toLowerCase().

```javascript
function solve(input) { return input.trim().toLowerCase(); }
```

**Go further:** Change one test input in your own experiments. Explain which part of your code determines the result.

## 15. Extract part of a string

slice returns part of a string without changing the original. The ending index is excluded. Negative indices count from the end. JavaScript string indices operate on UTF-16 code units, which is not always the same as a visible character.

**Challenge:** Return the first three code units of input.

**Hint:** Start at index 0 and stop before 3.

```javascript
function solve(input) { return input.slice(0, 3); }
```

**Go further:** Change one test input in your own experiments. Explain which part of your code determines the result.

## 16. Read an array

An array is an ordered collection. Indexing starts at zero, and length is a count rather than the last index. Accessing beyond the array returns undefined. Decide explicitly what an empty array should mean to the caller.

**Challenge:** Return the first item, or null if the array is empty.

**Hint:** Use length to check emptiness rather than truthiness of the first value.

```javascript
function solve(input) { return input.length ? input[0] : null; }
```

**Go further:** Change one test input in your own experiments. Explain which part of your code determines the result.

## 17. Copy before appending

Array spread makes a shallow copy. Adding an item to a new array avoids mutating a collection that another part of the program may share. Nested objects are still shared unless you deliberately copy them too.

**Challenge:** Return a new array with input.item appended to input.items.

**Hint:** Spread input.items inside a new array literal.

```javascript
function solve(input) { return [...input.items, input.item]; }
```

**Go further:** Change one test input in your own experiments. Explain which part of your code determines the result.

## 18. Transform with map

map calls a function for each element and returns an array of the results. It preserves array length for these dense examples. Use it to describe a transformation, not just to trigger side effects. Remember to return a result from a block-bodied callback.

**Challenge:** Return every input number doubled.

**Hint:** An arrow callback can return n * 2 implicitly.

```javascript
function solve(input) { return input.map(n => n * 2); }
```

**Go further:** Change one test input in your own experiments. Explain which part of your code determines the result.

## 19. Select with filter

filter keeps elements whose callback returns a truthy value. It does not modify the input array. A filter is a selection rule, not a transformation; the values that pass remain the original values or object references.

**Challenge:** Keep only numbers greater than zero.

**Hint:** Zero should not pass n > 0.

```javascript
function solve(input) { return input.filter(n => n > 0); }
```

**Go further:** Change one test input in your own experiments. Explain which part of your code determines the result.

## 20. Find one record

find returns the first matching element or undefined. This differs from filter, which always returns an array. A missing result should be handled before reading its properties. The nullish operator is useful when null is your explicit missing marker.

**Challenge:** Find the user with id input.id, or return null.

**Hint:** Call find on input.users, then supply ?? null.

```javascript
function solve(input) { return input.users.find(u => u.id === input.id) ?? null; }
```

**Go further:** Change one test input in your own experiments. Explain which part of your code determines the result.

## 21. Accumulate with reduce

reduce carries an accumulator through a collection. An explicit initial value makes empty-array behavior predictable. Name the accumulator by its role, such as total, instead of treating reduce as a clever shorthand for every loop.

**Challenge:** Return the sum of the input numbers, including 0 for empty input.

**Hint:** Start the accumulator at zero.

```javascript
function solve(input) { return input.reduce((total, n) => total + n, 0); }
```

**Go further:** Change one test input in your own experiments. Explain which part of your code determines the result.

## 22. Sort numbers correctly

sort mutates an array and defaults to string ordering. A numeric comparator returns a negative, zero or positive result. Copy first when the original order matters. Comparing numbers with a - b gives ascending order.

**Challenge:** Return input sorted numerically in ascending order without changing it.

**Hint:** Copy with spread, then use a numeric comparator.

```javascript
function solve(input) { return [...input].sort((a,b) => a-b); }
```

**Go further:** Change one test input in your own experiments. Explain which part of your code determines the result.

## 23. Check every or some

every checks whether all elements satisfy a predicate; some checks whether at least one does. every on an empty array is true because there is no counterexample. If your rule requires a non-empty set, validate length separately.

**Challenge:** Return whether every input number is at least zero.

**Hint:** Use every, not filter.

```javascript
function solve(input) { return input.every(n => n >= 0); }
```

**Go further:** Change one test input in your own experiments. Explain which part of your code determines the result.

## 24. Remove primitive duplicates

Set stores unique values and preserves insertion order. Spreading a Set back into an array is a compact way to remove repeated primitive values. Distinct objects with identical fields are still distinct identities in a Set.

**Challenge:** Return unique input numbers in their original order.

**Hint:** Construct a Set, then spread it into an array.

```javascript
function solve(input) { return [...new Set(input)]; }
```

**Go further:** Change one test input in your own experiments. Explain which part of your code determines the result.

## 25. Chain a useful data pipeline

A sequence of small array operations can describe a report clearly. First filter unwanted records, then transform the selected values. Avoid doing expensive or destructive work inside callbacks. Each intermediate array is a new collection in this exercise.

**Challenge:** Return the names of active users.

**Hint:** Filter on active, then map to name.

```javascript
function solve(input) { return input.filter(u => u.active).map(u => u.name); }
```

**Go further:** Change one test input in your own experiments. Explain which part of your code determines the result.

## 26. Read nested objects safely

Optional chaining stops a property lookup when the receiver is nullish. It does not catch arbitrary exceptions and does not create missing data. Combine it with a fallback when a missing nested value has a defined display meaning.

**Challenge:** Return input.profile.city, or Unknown when missing.

**Hint:** Use ?. for profile and ?? for the display fallback.

```javascript
function solve(input) { return input.profile?.city ?? "Unknown"; }
```

**Go further:** Change one test input in your own experiments. Explain which part of your code determines the result.

## 27. Destructure fields

Destructuring gives local names to fields or array entries. Defaults apply only when the value is undefined, not when it is null. This is useful for readable function bodies, but it does not validate the input shape by itself.

**Challenge:** Read name and role from input; default role to learner. Return name:role.

**Hint:** Use a default inside the destructuring pattern.

```javascript
function solve(input) { const {name, role="learner"} = input; return `${name}:${role}`; }
```

**Go further:** Change one test input in your own experiments. Explain which part of your code determines the result.

## 28. Update an object immutably

Object spread copies enumerable own properties into a new object. Later fields overwrite earlier fields. It is a shallow copy, so nested objects remain shared. Place the new field after the spread to replace the old value.

**Challenge:** Return a copy of input with done set to true.

**Hint:** Spread first, then set done.

```javascript
function solve(input) { return {...input, done:true}; }
```

**Go further:** Change one test input in your own experiments. Explain which part of your code determines the result.

## 29. Inspect object entries

Object.entries returns pairs of own enumerable string-keyed properties. Each pair contains a key and value. This makes object data usable in an array pipeline. Symbol keys and inherited properties are outside this method’s result.

**Challenge:** Return the keys whose values are true.

**Hint:** Filter entry pairs by value, then keep each key.

```javascript
function solve(input) { return Object.entries(input).filter(([k,v]) => v === true).map(([k]) => k); }
```

**Go further:** Change one test input in your own experiments. Explain which part of your code determines the result.

## 30. Serialize and parse JSON

JSON is a text data format, not executable JavaScript. JSON.parse reads JSON text and throws on invalid syntax. It does not validate the business meaning or schema. JSON cannot faithfully represent every JavaScript value, such as functions or undefined.

**Challenge:** Parse input JSON and return its name field.

**Hint:** Parse the string before accessing name.

```javascript
function solve(input) { return JSON.parse(input).name; }
```

**Go further:** Change one test input in your own experiments. Explain which part of your code determines the result.

## 31. Write a reusable function

A parameter is the local name used inside a function; an argument is the value supplied by a caller. Return values make functions composable. A focused helper is easier to test because it has one explicit responsibility.

**Challenge:** Define double(n) and return double(input).

**Hint:** Define the helper inside solve, then call it.

```javascript
function solve(input) { function double(n) { return n * 2; } return double(input); }
```

**Go further:** Change one test input in your own experiments. Explain which part of your code determines the result.

## 32. Use a default parameter

Default parameters provide a value when an argument is omitted or undefined. They are not general validation. Passing null is an explicit value and does not trigger the default. Choose defaults that make the function convenient without hiding mistakes.

**Challenge:** Use greet(name="friend") to greet input.name.

**Hint:** Give the helper parameter a default value.

```javascript
function solve(input) { function greet(name="friend") { return `Hi ${name}`; } return greet(input.name); }
```

**Go further:** Change one test input in your own experiments. Explain which part of your code determines the result.

## 33. Collect rest arguments

A rest parameter gathers remaining arguments into an array. Spread does the reverse when calling: it expands an iterable into arguments. These similar-looking syntaxes have different roles depending on where they appear.

**Challenge:** Create sum(...numbers) and sum all numbers in input.

**Hint:** Use rest in the helper definition and spread in its call.

```javascript
function solve(input) { const sum = (...numbers) => numbers.reduce((a,b)=>a+b,0); return sum(...input); }
```

**Go further:** Change one test input in your own experiments. Explain which part of your code determines the result.

## 34. See a closure remember

A closure retains access to bindings from its surrounding scope. This allows a function to keep private state between calls. Each invocation of the factory creates a separate environment; it does not share one global counter with all instances.

**Challenge:** Create a counter starting at input. Call it twice and return both results.

**Hint:** The returned function updates a let binding in its outer function.

```javascript
function solve(input) { function makeCounter(start) { let n=start; return () => ++n; } const next=makeCounter(input); return [next(),next()]; }
```

**Go further:** Change one test input in your own experiments. Explain which part of your code determines the result.

## 35. Keep pure transformations predictable

A pure data transformation returns a result from its inputs without changing external state. Purity is not required everywhere, but separating calculation from side effects makes testing easier. A UI can calculate its next state first and render it afterward.

**Challenge:** Return a copy of the numbers increased by input.amount.

**Hint:** Return a mapped array; do not modify the input array.

```javascript
function solve(input) { return input.numbers.map(n => n + input.amount); }
```

**Go further:** Change one test input in your own experiments. Explain which part of your code determines the result.

## 36. Trace a for loop

A for loop has initialization, a continuation test and an update step. Check the boundary carefully: <= includes the endpoint. An accumulator changes each iteration. The visual trace tool in this studio lets you step through this exact pattern.

**Challenge:** Sum the integers from 1 through input. Input is a nonnegative integer.

**Hint:** Start total at 0 and include input in the loop.

```javascript
function solve(input) { let total=0; for(let i=1;i<=input;i++){ total+=i; } return total; }
```

**Go further:** Change one test input in your own experiments. Explain which part of your code determines the result.

## 37. Iterate values with for…of

for…of reads values from an iterable such as an array. for…in instead iterates enumerable property names, which is often the wrong choice for array data. Choose syntax that makes it obvious whether you want a value or an index.

**Challenge:** Use a loop to count numbers greater than 10.

**Hint:** Loop over values and increment a counter only when n > 10.

```javascript
function solve(input) { let count=0; for(const n of input){ if(n>10) count++; } return count; }
```

**Go further:** Change one test input in your own experiments. Explain which part of your code determines the result.

## 38. Stop searching with break

break exits the nearest loop. It is useful when later values cannot affect the result. A loop that must always terminate needs a bounded collection or a progress condition. This exercise chooses the first negative value rather than collecting all of them.

**Challenge:** Return the first negative number, or null.

**Hint:** Set the result and break when you find a negative number.

```javascript
function solve(input) { let found=null; for(const n of input){ if(n<0){ found=n; break; } } return found; }
```

**Go further:** Change one test input in your own experiments. Explain which part of your code determines the result.

## 39. Catch a recoverable error

try catches exceptions thrown in its body. Use catch to handle failures you can meaningfully recover from. Do not silently hide unrelated errors. JSON parsing is a useful boundary because malformed input is a predictable failure case.

**Challenge:** Parse input JSON; return null if parsing fails.

**Hint:** Put only the parsing operation inside try.

```javascript
function solve(input) { try { return JSON.parse(input); } catch(error) { return null; } }
```

**Go further:** Change one test input in your own experiments. Explain which part of your code determines the result.

## 40. Validate a tiny form

Form inputs often arrive as strings and need normalization. Validate a specific contract rather than claiming a small pattern validates every real-world address. This exercise checks only a non-empty name and an age of at least 18 after conversion.

**Challenge:** Return true when trimmed name is nonempty and numeric age is >=18.

**Hint:** Combine a trimmed length check and a numeric comparison.

```javascript
function solve(input) { return input.name.trim().length > 0 && Number(input.age) >= 18; }
```

**Go further:** Change one test input in your own experiments. Explain which part of your code determines the result.

## 41. Wait for a promise

A promise represents a future outcome. async functions always return a promise. await pauses that function until the awaited value settles; it does not block the entire page. The course runner waits for the promise returned by solve.

**Challenge:** Await Promise.resolve(input), then return the value plus 1.

**Hint:** Make solve async and await the promise.

```javascript
async function solve(input) { const value=await Promise.resolve(input); return value+1; }
```

**Go further:** Change one test input in your own experiments. Explain which part of your code determines the result.

## 42. Handle a rejected promise

A rejection becomes an exception at an await expression. A nearby try/catch can map that failure into a clear fallback. Without awaiting inside try, a later rejection may escape that catch. Use fallbacks deliberately rather than pretending an operation succeeded.

**Challenge:** If input is false, reject and return offline; otherwise return ready.

**Hint:** Await the rejected promise within try.

```javascript
async function solve(input) { try { if(!input) await Promise.reject(new Error("offline")); return "ready"; } catch(error) { return "offline"; } }
```

**Go further:** Change one test input in your own experiments. Explain which part of your code determines the result.

## 43. Coordinate independent promises

Promise.all waits for every supplied promise and preserves input order in its result. It rejects if any supplied promise rejects. It is useful for independent operations, but it is not a concurrency limiter for an unbounded workload.

**Challenge:** Resolve each input number doubled with Promise.all and return the array.

**Hint:** Map to promises, then await Promise.all.

```javascript
async function solve(input) { return await Promise.all(input.map(async n => n*2)); }
```

**Go further:** Change one test input in your own experiments. Explain which part of your code determines the result.

## 44. Understand a response contract

Real fetch resolves on many HTTP error statuses, so successful transport is not proof of a successful operation. This exercise uses an injected plain response fixture, not network access. Separate the status check from reading the data.

**Challenge:** Return input.data when input.ok is true; otherwise return request failed.

**Hint:** Check ok before using data. No fetch call is needed here.

```javascript
async function solve(input) { if(!input.ok) return "request failed"; return input.data; }
```

**Go further:** Change one test input in your own experiments. Explain which part of your code determines the result.

## 45. Observe microtask order

Synchronous code runs before promise callbacks queued for the microtask checkpoint. await gives already queued promise reactions an opportunity to run. Understanding this ordering explains why a callback may see a value after later synchronous statements have changed it.

**Challenge:** Return an array recording start, end, then promise in that order using a promise callback.

**Hint:** Push end synchronously before awaiting job.

```javascript
async function solve(input) { const events=["start"]; const job=Promise.resolve().then(()=>events.push("promise")); events.push("end"); await job; return events; }
```

**Go further:** Change one test input in your own experiments. Explain which part of your code determines the result.

## 46. Project: shopping cart total

Combine filtering and reduction into a small business rule. Only included items contribute, and each contributes price times quantity. Keep currency in whole minor units when possible. The input here uses integer cents, so the result also represents cents.

**Challenge:** Sum price * quantity for items with included true.

**Hint:** Filter the cart, then reduce its line totals.

```javascript
function solve(input) { return input.filter(x=>x.included).reduce((sum,x)=>sum+x.price*x.quantity,0); }
```

**Go further:** Change one test input in your own experiments. Explain which part of your code determines the result.

## 47. Project: search results

A search filter should define normalization, matching and result shape. This simple search ignores case and trims the query. It matches substrings, not words or fuzzy similarity. Returning the original records keeps display values unchanged.

**Challenge:** Return items whose name includes the trimmed query, ignoring case.

**Hint:** Normalize both the query and comparison text.

```javascript
function solve(input) { const q=input.query.trim().toLowerCase(); return input.items.filter(x=>x.name.toLowerCase().includes(q)); }
```

**Go further:** Change one test input in your own experiments. Explain which part of your code determines the result.

## 48. Project: group a dashboard

A frequency report turns individual records into counts. Map avoids collisions with inherited object names and makes the lookup explicit. Convert entries to a plain object for a portable result. Categories here are strings supplied by the exercise.

**Challenge:** Count how many times each input label occurs.

**Hint:** Use a Map and increment each label’s count.

```javascript
function solve(input) { const counts=new Map(); for(const label of input) counts.set(label,(counts.get(label)??0)+1); return Object.fromEntries(counts); }
```

**Go further:** Change one test input in your own experiments. Explain which part of your code determines the result.

## 49. Project: task state reducer

A reducer describes a next state from a previous state and an action. It should not mutate the previous state. This pattern supports UI rendering, undo history and straightforward tests. Here toggling changes only the matching record.

**Challenge:** Return tasks with done toggled for input.id; leave other tasks unchanged.

**Hint:** Map to a new collection and copy the matching object.

```javascript
function solve(input) { return input.tasks.map(t=>t.id===input.id?{...t,done:!t.done}:t); }
```

**Go further:** Change one test input in your own experiments. Explain which part of your code determines the result.

## 50. Capstone: clean and rank a report

Bring several skills together in an explicit pipeline. Keep only valid numeric scores, normalize names and sort by descending score. Never let numeric-looking strings silently pass as numbers unless conversion is part of your stated contract. Empty names are excluded after trimming.

**Challenge:** Keep records with nonempty trimmed names and finite numeric scores; return cleaned records sorted highest score first.

**Hint:** Filter invalid records, map to clean fields, then sort numerically descending.

```javascript
function solve(input) { return input.filter(x=>x.name.trim() && Number.isFinite(x.score)).map(x=>({name:x.name.trim(),score:x.score})).sort((a,b)=>b.score-a.score); }
```

**Go further:** Change one test input in your own experiments. Explain which part of your code determines the result.
