Modern JavaScript often puts const, destructuring, arrow functions, spread, classes, modules and async/await into a few lines. Memorising isolated definitions will not help when you must trace the whole flow or debug one wrong value. A single score-report flow connects synchronous transformation to asynchronous control and exposes mistakes that produce wrong values. If the browser and programming path is still new, start with the Coding & Skills collection and return with a console open.
What ES6+ means and how to run the examples
ES6 commonly means ECMAScript 2015, while ES6+ includes useful additions from later editions. The features solve different jobs: declare state, copy data, write functions, model objects, split modules and wait for asynchronous work.
Single-block snippets run in a fresh browser DevTools console. Predict each output before pressing Enter. The ES module run uses two JavaScript files and an index.html file. If HTML or browser tools are unfamiliar, Web Technologies for Teaching CS Exams provides the prerequisite bridge.
Block scope, template literals, destructuring and spread
Use let for reassignment and const otherwise. Both are block-scoped, unlike function-scoped var. Trace let stage = "start"; { const stage = "quiz"; console.log(stage); } console.log(stage);: it prints quiz, then start. A const object's properties can change, but its binding cannot point at another object.
Now follow one object through several ES6 features:
const attempt = {
learner: "Riya",
scores: [72, 88, 91],
settings: { bonus: 5, showRank: false }
};
const { learner, scores, settings: { bonus } } = attempt;
const extendedScores = [...scores, 95];
const adjustedScores = extendedScores.map(score => score + bonus);
console.log(`${learner}: ${adjustedScores.join(", ")}`);Exact output: Riya: 77, 93, 96, 100.
Destructuring extracts learner, scores and nested bonus. Spread copies [72, 88, 91] and appends 95, producing [72, 88, 91, 95]. The arrow callback adds 5, so map returns [77, 93, 96, 100]. The template literal joins those values with Riya's name. Spread and map each create a new array, leaving the original scores unchanged.
JavaScript Destructuring and Spread: Syntax, Rest Patterns, and Worked Examples isolates binding patterns, rest placement, property overwrite order and nested-copy traps. The score-report flow connects those mechanics to arrows, classes, modules and asynchronous work.

Arrow functions, default parameters and rest parameters
Arrow functions keep small transformations compact. A default applies only when an argument is missing or undefined:
const addBonus = (score, bonus = 5) => score + bonus;
console.log(addBonus(72));
console.log(addBonus(72, 2));Exact output: 77, then 74. The expression after => is returned implicitly. addBonus(72, undefined) uses 5, while addBonus(72, 0) uses 0.
Rest and spread use the same ... spelling but do opposite jobs:
const adjustedScores = [77, 93, 96, 100];
const average = (...values) =>
values.reduce((sum, value) => sum + value, 0) / values.length;
console.log(average(...adjustedScores));Exact output: 91.5. Rest collects arguments into values; spread expands the array at the call site. The calculation is 77 + 93 + 96 + 100 = 366, then 366 / 4 = 91.5. The reduce accumulator starts at 0. Use an explicit return in a block-bodied arrow. Arrows inherit the surrounding this, so they do not replace methods that need a dynamic receiver.
Classes and ES modules without hiding the underlying data
Modules separate reusable logic from the code that consumes it. Create score.js:
export const average = (...values) =>
values.reduce((sum, value) => sum + value, 0) / values.length;The module now provides a named export called average and prints nothing.
Create report.js beside it:
import { average } from "./score.js";
class AttemptReport {
constructor(learner, scores) {
this.learner = learner;
this.scores = scores;
}
summary() {
return `${this.learner}: ${average(...this.scores)}`;
}
}
const report = new AttemptReport("Riya", [77, 93, 96, 100]);
console.log(report.summary());Exact output: Riya: 91.5.
Load it from index.html:
<script type="module" src="./report.js"></script>The browser loads report.js, resolves the named import from score.js, and prints Riya: 91.5. Braces identify a named import, and module scope prevents accidental globals. new creates an instance, the constructor stores its initial values, and summary reads them through this. The method is shared through the class prototype and delegates the arithmetic to average. JavaScript class is cleaner syntax over its prototype-based object model.
Promises and async/await: trace the order, not just the syntax
JavaScript Event Loop Tutorial: Trace Tasks, Microtasks, and Timers owns task, microtask and timer ordering; Async Await in JavaScript: Sequential, Parallel and Error Examples owns sequential versus parallel workflows, loops and recovery. A single score dependency needs one Promise chain, one await continuation and one rejection path.
For asynchronous code, predict the log order before reading the syntax:
const loadScore = () => Promise.resolve({ learner: "Riya", score: 88 });
console.log("1: request");
loadScore()
.then(({ learner, score }) => `${learner}: ${score + 5}`)
.then(message => console.log(`3: ${message}`));
console.log("2: continue");Exact output: 1: request, then 2: continue, then 3: Riya: 93. The first log runs immediately. loadScore() returns a fulfilled promise, but .then registers a microtask. JavaScript reaches the final synchronous log before the callback calculates 88 + 5 = 93. Its returned string fulfils the next promise, whose callback prints the third line.
async/await expresses the same dependency in a more sequential form:
const loadScore = () => Promise.resolve({ learner: "Riya", score: 88 });
async function buildMessage() {
const { learner, score } = await loadScore();
return `${learner}: ${score + 5}`;
}
buildMessage().then(console.log);Exact eventual output: Riya: 93. An async function returns a promise. await suspends only buildMessage, returns control to the caller, and resumes it after loadScore settles. Returning the string fulfils the promise, so the final .then logs it.
Handle rejection at the point where recovery makes sense:
async function showFailure() {
try {
await Promise.reject(new Error("score unavailable"));
} catch (error) {
console.log(error.message);
}
}
showFailure();Exact eventual output: score unavailable.
The rejection makes await throw, and catch turns it into a controlled message.

Later everyday features and the mistakes they prevent
Optional chaining stops property access when the value to its left is nullish. Nullish coalescing supplies a fallback only for null or undefined:
const profile = { attempts: 0, preferences: { darkMode: false } };
console.log(profile.preferences?.darkMode ?? true);
console.log(profile.rank?.label ?? "Unranked");
console.log(profile.attempts ?? 1);
console.log(profile.attempts || 1);Exact output: false, Unranked, 0, then 1. Unlike ||, ?? preserves valid falsy values such as 0 and false. Optional chaining protects only the chain where it appears, and its root variable must exist.
Several common bugs follow equally precise rules:
Accessing a
letbinding before its declaration triggers the temporal dead zone.{ ...attempt }makes a shallow copy, so nested objects remain shared.Extracting an object method can lose its
thisreceiver. Call it through the object or bind the receiver.A block-bodied arrow callback returns
undefinedunless you writereturn.Forgetting
awaitleaves a Promise where the resolved value was expected.
For the shallow-copy trace, const copy = { ...attempt }; copy.settings.bonus = 10; makes both bonus values 10. To copy settings too, use const copy = { ...attempt, settings: { ...attempt.settings } };. Any still-deeper objects remain shared.
Output exercises and the next step
Predict these before reading the answers:
Trace
let count = 1; { let count = 3; count += 2; console.log(count); } console.log(count);.Predict both bonus values after the shallow-copy mutation in the previous section.
Trace
console.log("start"); Promise.resolve().then(() => console.log("done")); console.log("end");.
Answer key: the first prints 5, then 1. The second leaves both bonus values at 10. The third prints start, end, done, because the promise callback waits for the synchronous stack. The three tasks test block scope, reference sharing and microtask order, so explain the rule as well as the output.
The short version: prefer const; choose let for intentional reassignment; destructure deliberately; remember that spread is shallow; use arrows carefully around this; and trace promise scheduling. Continue with the Complete JavaScript course for structured language practice. Then rebuild the score flow with your own data and explain each value before running it.




