Senior 6 min · March 05, 2026

ES6+ Features Explained — The Why, When, and Real-World How

ES6+ JavaScript features demystified — learn arrow functions, destructuring, async/await, and more with real-world patterns and interview-ready explanations.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • ES6+ is a set of modern JavaScript syntax and features that reduce boilerplate and eliminate common bugs.
  • Key features: arrow functions, let/const, destructuring, spread/rest, template literals, Promises, async/await, modules.
  • Performance: arrow functions don't bind this — saves memory per callback, but no performance difference in execution.
  • Production insight: Forgetting to handle Promise rejections crashes Node.js processes in newer versions.
  • Biggest mistake: Using arrow functions as object methods breaks this — always use regular function syntax for methods.
Plain-English First

Imagine you used to pack for a trip by laying every single shirt, sock, and shoe out one by one, naming each item out loud before putting it in the bag. ES6+ is like getting a smart packing organizer — it lets you grab a whole outfit at once, label compartments automatically, and even pack for tomorrow's trip while you sleep. It doesn't change what you're doing (writing JavaScript), it just removes the tedious, error-prone busywork so you can focus on actually building things.

JavaScript before ES6 was like cooking in a kitchen where you had to make every single utensil by hand before you could start the recipe. You could do it — millions of developers did — but an enormous chunk of your day was fighting ceremony instead of solving real problems. ES6 (released in 2015) and the yearly spec updates that followed (ES7, ES8... collectively called ES6+) rewired how modern JavaScript is written. Every production codebase you'll encounter today — React apps, Node APIs, browser extensions — is built on these features.

let & const vs var — Why Block Scope Changed Everything

Before ES6, var was the only way to declare a variable. The problem? var is function-scoped, not block-scoped. That means a variable declared inside an if block leaks out into the surrounding function. In large codebases this causes bugs that are genuinely hard to trace — you change a variable inside a loop, and suddenly something outside the loop has a different value than you expected.

let and const brought block scoping to JavaScript. A variable declared with let or const inside curly braces {} lives and dies inside those braces. Nothing outside can see it. const goes one step further — it prevents reassignment of the binding itself, which makes your intent clear: 'this value should not change.'

Use const by default. Reach for let only when you know you'll reassign (like a loop counter or an accumulator). Avoid var in new code entirely — there's no modern scenario where var is the better choice.

blockScope.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// ----- THE VAR PROBLEM -----
function calculateDiscount_OLD(price) {
  if (price > 100) {
    var discount = 20; // declared inside the if-block
  }
  // 'discount' leaks OUT of the if-block because var is function-scoped
  console.log('var discount outside block:', discount); // 20 (!) — or undefined if price <= 100
}
calculateDiscount_OLD(150);

// ----- THE let FIX -----
function calculateDiscount_NEW(price) {
  if (price > 100) {
    let discount = 20; // block-scoped — stays inside the if-block
  }
  // This line would throw: ReferenceError: discount is not defined
  // console.log(discount); // safely commented out — the error IS the feature!
  console.log('let prevents the accidental leak — discount is not visible here');
}
calculateDiscount_NEW(150);

// ----- const FOR VALUES THAT SHOULD NOT CHANGE -----
const TAX_RATE = 0.08; // Tax rate won't change during the program
const itemPrice = 49.99;
const totalPrice = itemPrice + itemPrice * TAX_RATE;
console.log('Total with tax:', totalPrice.toFixed(2)); // 53.99

// TAX_RATE = 0.10; // Uncommenting this throws: TypeError: Assignment to constant variable

// ----- IMPORTANT: const with objects -----
const userProfile = { name: 'Alice', role: 'admin' };
userProfile.role = 'editor'; // This IS allowed — we're mutating the object, not rebinding the variable
console.log('Updated role:', userProfile.role); // editor
// userProfile = {}; // THIS would throw — you can't rebind the const variable itself
Output
var discount outside block: 20
let prevents the accidental leak — discount is not visible here
Total with tax: 53.99
Updated role: editor
Watch Out: const Doesn't Mean Immutable
const prevents you from reassigning the variable to a new value, but it does NOT freeze the object or array at that variable. You can still push to a const array or change a const object's properties. If you need a truly immutable object, use Object.freeze() — but know that freeze is only one level deep.
Production Insight
A production incident: a developer used var in a for loop, and the loop index leaked into the global scope, overwriting a critical configuration variable. The bug was intermittent and only appeared under heavy load because of race conditions with the loop timing.
Fix: Use let for loop counters. Always lint against var usage.
Key rule: Block-scoping eliminates an entire class of variable-sharing bugs that were notoriously hard to debug in ES5.
Key Takeaway
const by default, let when you must reassign
var leaks — never use it in modern code
const objects are mutable — use Object.freeze() only when you need deep immutability

Arrow Functions and Destructuring — Less Noise, More Signal

Arrow functions (=>) aren't just a shorter way to write a function — they deliberately don't bind their own this. In traditional functions, this depends on how the function is called, which is why you'd see code like var self = this; or .bind(this) everywhere. Arrow functions inherit this from the surrounding lexical scope, eliminating an entire class of confusing bugs.

Destructuring is the other daily-use feature that transforms how readable your code is. Instead of writing const userName = user.name; const userAge = user.age; on separate lines, you extract multiple values from an object or array in a single, expressive line. It reads almost like English: 'from this user object, give me the name and age.'

These two features combine constantly in real code — you'll see arrow functions as array callbacks and destructuring in function parameters. Learning them together is the fastest path to reading and writing modern JavaScript fluently.

arrowAndDestructure.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// ----- THE `this` PROBLEM ARROW FUNCTIONS SOLVE -----
const timer = {
  message: 'Time is up!',

  // Old-style function: `this` depends on how the function is called
  startOld: function () {
    setTimeout(function () {
      // Inside a regular callback, `this` is no longer the timer object
      // In strict mode it's undefined; in browsers it's the window object
      console.log('Old way — this.message:', this.message); // undefined (or crash)
    }, 100);
  },

  // Arrow function: `this` is inherited from startNew's scope (the timer object)
  startNew: function () {
    setTimeout(() => {
      console.log('Arrow way — this.message:', this.message); // 'Time is up!'
    }, 100);
  },
};
timer.startOld();
timer.startNew();

// ----- ARROW FUNCTIONS AS ARRAY CALLBACKS -----
const products = [
  { name: 'Keyboard', price: 79 },
  { name: 'Monitor', price: 299 },
  { name: 'Mouse', price: 45 },
];

// map with an arrow function — clean, one-liner transformation
const productNames = products.map((product) => product.name);
console.log('Product names:', productNames); // ['Keyboard', 'Monitor', 'Mouse']

// filter — only items under $100
const affordableProducts = products.filter((product) => product.price < 100);
console.log('Under $100:', affordableProducts.map((p) => p.name)); // ['Keyboard', 'Mouse']

// ----- OBJECT DESTRUCTURING -----
const orderDetails = {
  orderId: 'ORD-8821',
  customer: 'Bob Martinez',
  total: 124.5,
  status: 'shipped',
};

// Extract only what you need — notice the rename: status -> orderStatus
const { orderId, customer, status: orderStatus } = orderDetails;
console.log(`Order ${orderId} for ${customer} is ${orderStatus}`);
// Order ORD-8821 for Bob Martinez is shipped

// ----- ARRAY DESTRUCTURING -----
const [firstPlace, secondPlace, , fourthPlace] = ['Alice', 'Bob', 'Carol', 'Dave'];
console.log('Winner:', firstPlace);   // Alice
console.log('Runner-up:', secondPlace); // Bob
console.log('4th place:', fourthPlace); // Dave (skipped Carol with the empty comma)

// ----- DESTRUCTURING IN FUNCTION PARAMETERS (very common in React) -----
function renderUserCard({ name, role = 'viewer', avatarUrl = '/default-avatar.png' }) {
  // Default values in destructuring mean we never get undefined for missing fields
  console.log(`Rendering card for ${name} (${role}) — avatar: ${avatarUrl}`);
}

renderUserCard({ name: 'Alice', role: 'admin' });
// Rendering card for Alice (admin) — avatar: /default-avatar.png
renderUserCard({ name: 'Charlie' });
// Rendering card for Charlie (viewer) — avatar: /default-avatar.png
Output
Old way — this.message: undefined
Arrow way — this.message: Time is up!
Product names: [ 'Keyboard', 'Monitor', 'Mouse' ]
Under $100: [ 'Keyboard', 'Mouse' ]
Order ORD-8821 for Bob Martinez is shipped
Winner: Alice
Runner-up: Bob
4th place: Dave
Rendering card for Alice (admin) — avatar: /default-avatar.png
Rendering card for Charlie (viewer) — avatar: /default-avatar.png
Pro Tip: Don't Use Arrow Functions as Object Methods
Because arrow functions don't bind their own this, using them as methods directly on an object will cause this to point to the outer scope (often undefined in modules, or the global object in scripts) instead of the object itself. Always use regular function syntax for object methods and class methods. Arrow functions shine as callbacks and helper functions — not as the primary method definition.
Production Insight
A common production failure: arrow function used as an object method in a React component's event handler. this.setState throws 'undefined' because this is the window or undefined. The fix: always use class method syntax or bind in constructor.
Debug tip: If you see this is undefined in a method, check the definition — arrow function? replace with function() {} or method shorthand.
Rule: Arrow functions for callbacks, regular functions for methods.
Key Takeaway
Arrow functions inherit this lexically — great for callbacks, deadly for methods
Destructuring reduces boilerplate and makes code self-documenting
Combine them: arrow callbacks with destructured parameters for clean data transformations

Spread, Rest, and Template Literals — Clean Data Handling

The spread operator (...) lets you 'unpack' an array or object into individual pieces. Think of it like opening a box and laying everything out on a table. Its sibling, the rest parameter, does the opposite — it gathers a variable number of arguments into an array. Same syntax, opposite directions, and understanding both together prevents a lot of confusion.

These aren't just convenience features. Spread is the backbone of immutable data patterns in React and Redux — instead of mutating an existing object, you spread it into a new one with your changes. That one pattern is responsible for making component state predictable across thousands of React apps.

Template literals (backtick strings) replace string concatenation entirely. They support multiline strings without escape characters, and embedded expressions with ${} that can hold any JavaScript expression — not just variables.

spreadRestTemplates.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// ----- SPREAD WITH ARRAYS -----
const northernCities = ['Oslo', 'Stockholm', 'Helsinki'];
const southernCities = ['Rome', 'Athens', 'Madrid'];

// Combine two arrays without mutation
const allCities = [...northernCities, 'Paris', ...southernCities];
console.log('All cities:', allCities);
// ['Oslo', 'Stockholm', 'Helsinki', 'Paris', 'Rome', 'Athens', 'Madrid']

// Clone an array (not a reference — a new array)
const citiesCopy = [...northernCities];
citiesCopy.push('Reykjavik');
console.log('Original unchanged:', northernCities); // Oslo, Stockholm, Helsinki
console.log('Clone has new city:', citiesCopy);      // Oslo, Stockholm, Helsinki, Reykjavik

// ----- SPREAD WITH OBJECTS (the React state update pattern) -----
const currentUserSettings = {
  theme: 'dark',
  language: 'en',
  notificationsEnabled: true,
};

// Create a new settings object with just the theme changed
// The spread copies all existing keys, then the last key 'wins' for overrides
const updatedSettings = { ...currentUserSettings, theme: 'light' };
console.log('Original settings:', currentUserSettings.theme); // dark — untouched
console.log('Updated settings:', updatedSettings.theme);      // light

// ----- REST PARAMETERS -----
function calculateShippingCost(baseRate, ...itemWeights) {
  // `itemWeights` collects all arguments after the first into a real array
  const totalWeight = itemWeights.reduce((sum, weight) => sum + weight, 0);
  const shippingCost = baseRate + totalWeight * 0.5;
  console.log(`Items: ${itemWeights.length}, Total weight: ${totalWeight}kg, Cost: $${shippingCost.toFixed(2)}`);
}

calculateShippingCost(5, 1.2, 0.8, 3.5); // 3 items
// Items: 3, Total weight: 5.5kg, Cost: $7.75
calculateShippingCost(5, 0.5);            // 1 item
// Items: 1, Total weight: 0.5kg, Cost: $5.25

// ----- TEMPLATE LITERALS -----
const orderSummary = {
  id: 'ORD-4492',
  itemCount: 3,
  total: 87.49,
  deliveryDate: 'Thursday',
};

// Multiline template literal — no more \n escape sequences
const confirmationEmail = `
Hi there,

Your order ${orderSummary.id} has been confirmed.
You ordered ${orderSummary.itemCount} items for a total of $${orderSummary.total.toFixed(2)}.
Expected delivery: ${orderSummary.deliveryDate}.

Thank you for shopping with us!
`.trim();

console.log(confirmationEmail);
// Full email block with real line breaks — no string concatenation needed
Output
All cities: [ 'Oslo', 'Stockholm', 'Helsinki', 'Paris', 'Rome', 'Athens', 'Madrid' ]
Original unchanged: [ 'Oslo', 'Stockholm', 'Helsinki' ]
Clone has new city: [ 'Oslo', 'Stockholm', 'Helsinki', 'Reykjavik' ]
Original settings: dark
Updated settings: light
Items: 3, Total weight: 5.5kg, Cost: $7.75
Items: 1, Total weight: 0.5kg, Cost: $5.25
Hi there,
Your order ORD-4492 has been confirmed.
You ordered 3 items for a total of $87.49.
Expected delivery: Thursday.
Thank you for shopping with us!
Interview Gold: Spread Creates Shallow Copies
Spreading an object or array only copies one level deep. If your object has a nested object as a value, the spread gives you a new outer object but both the original and the copy point to the same nested object. Changing that nested object affects both. For deep cloning, use structuredClone() (modern) or JSON.parse(JSON.stringify(obj)) (older workaround, with caveats around dates and functions).
Production Insight
Real-world failure: A developer used spread to copy a config object before mutation, but the nested 'database' config was shared. Changing the replica's database URL in the copy also changed the original — causing both to point to staging instead of production in a blue-green deployment.
Fix: Use structuredClone() for deep config copies or manually clone nested properties.
Rule: Spread is shallow — assume all nested references are shared until proven otherwise.
Key Takeaway
Spread creates shallow copies — nested objects are still references
Rest parameters replace arguments with a real array
Template literals eliminate concatenation confusion — use them everywhere

The for...of Loop — Iterating Over Iterables

Before ES6, iterating over arrays required a for loop with an index, or the forEach method (which doesn't support break/continue/return). For objects, you'd use for...in, which iterates over enumerable property names including inherited ones, often causing bugs.

The for...of loop (ES6) solves this by working directly with iterables — arrays, strings, Maps, Sets, NodeLists, and any object implementing the iterable protocol. It gives you values, not indices, and supports break, continue, and return. It's the cleanest way to loop through built-in data structures.

Use for...of when you need the values of an array or any iterable. Use for...in only when you need object keys (and you're sure about property enumeration order).

forOf.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// ----- for...of WITH ARRAYS -----
const colors = ['red', 'green', 'blue'];
for (const color of colors) {
  console.log(color);
}
// red
// green
// blue

// ----- for...of WITH STRINGS -----
const greeting = 'Hi!';
for (const char of greeting) {
  console.log(char);
}
// H
// i
// !

// ----- for...of WITH MAPS -----
const userMap = new Map([
  ['name', 'Alice'],
  ['role', 'admin'],
]);
for (const [key, value] of userMap) {
  console.log(`${key}: ${value}`);
}
// name: Alice
// role: admin

// ----- for...of WITH SETS -----
const uniqueIds = new Set([101, 102, 103, 101]);
for (const id of uniqueIds) {
  console.log(id);
}
// 101
// 102
// 103

// ----- BREAK and CONTINUE WORK -----
const numbers = [1, 2, 3, 4, 5];
for (const n of numbers) {
  if (n === 3) continue;
  if (n === 5) break;
  console.log(n);
}
// 1
// 2
// 4

// ----- DO NOT CONFUSE with for...in -----
const obj = { a: 1, b: 2 };
for (const key in obj) {
  console.log(key, obj[key]); // 'a' 1, 'b' 2 — but not recommended for arrays
}
const arr = [10, 20, 30];
for (const index in arr) {
  console.log(index, arr[index]); // '0' 10, '1' 20, '2' 30 — uses index as string, includes inherited props
}
Output
red
green
blue
H
i
!
name: Alice
role: admin
101
102
103
1
2
4
a 1
b 2
0 10
1 20
2 30
for...of vs forEach
forEach does not support break or continue — you'd need to throw an exception or use a return (which only exits the callback). for...of supports all loop control statements and is often more performant for large arrays because it avoids the overhead of a callback per iteration.
Production Insight
Avoid using for...in on arrays in production — it iterates over enumerable properties (including array indices as strings) and can pick up inherited enumerable properties from prototypes. Always use for...of for arrays. A common bug: iterating over a NodeList with for...in returns unexpected properties because the NodeList inherits from Object.prototype.
Rule: Use for...of for iterables, for...in only for plain objects when you need keys.
Key Takeaway
for...of iterates over values of any iterable (arrays, strings, Maps, Sets)
Supports break, continue, return — unlike forEach
Avoid for...in for arrays; use it only for object property enumeration

Map and Set — New Data Structures for Modern JavaScript

ES6 introduced two new built-in data structures: Map (key-value pairs where keys can be any type) and Set (unique values of any type). They fill gaps that plain objects and arrays left open.

Map vs Object: A Map preserves insertion order, performs better with frequent additions/removals, and accepts any value as a key (including objects, functions, NaN). Objects convert keys to strings (e.g., { 'true': 1 }), while Maps keep the original type.

Set vs Array: A Set automatically enforces uniqueness — no duplicate values. It provides .has(value) in O(1) time, which is much faster than Array.includes() (O(n)). Use Set when you need to track unique items and test membership.

WeakMap and WeakSet hold 'weak' references — they don't prevent garbage collection of keys. Use them when you need to associate data with objects without preventing their cleanup (e.g., caching DOM elements in a single-page app).

mapSet.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// ----- MAP: KEY-VALUE WITH ANY KEY TYPE -----
const userRoles = new Map();
userRoles.set(101, 'admin');
userRoles.set('user-102', 'editor');
userRoles.set({ id: 103 }, 'viewer'); // object as key

console.log(userRoles.get(101));   // 'admin'
console.log(userRoles.has('user-102')); // true
console.log(userRoles.size);       // 3

// Iteration preserves insertion order
for (const [id, role] of userRoles) {
  console.log(`User ${id}: ${role}`);
}
// User 101: admin
// User user-102: editor
// User [object Object]: viewer

// ----- SET: UNIQUE VALUES -----
const visitorIps = new Set();
visitorIps.add('192.168.1.1');
visitorIps.add('10.0.0.1');
visitorIps.add('192.168.1.1'); // duplicate ignored
console.log(visitorIps.size);   // 2
console.log(visitorIps.has('10.0.0.1')); // true

// Convert Set to Array when needed
const ipArray = [...visitorIps];
console.log(ipArray); // ['192.168.1.1', '10.0.0.1']

// ----- WEAKMAP: KEYS MUST BE OBJECTS, ALLOWS GC -----
const cache = new WeakMap();
let obj = { name: 'temp' };
cache.set(obj, 'cached data');
obj = null; // 'cached data' is now eligible for garbage collection

// ----- WEAKSET: SIMILAR FOR SET -----
const activeUsers = new WeakSet();
let user = { id: 1 };
activeUsers.add(user);
user = null; // user is removed from activeUsers when GC runs

// ----- PRACTICAL USE: REMOVING DUPLICATES FROM AN ARRAY -----
const numbersWithDuplicates = [1, 2, 3, 2, 4, 1, 5];
const uniqueNumbers = [...new Set(numbersWithDuplicates)];
console.log(uniqueNumbers); // [1, 2, 3, 4, 5]
Output
User 101: admin
User user-102: editor
User [object Object]: viewer
2
[ '192.168.1.1', '10.0.0.1' ]
[ 1, 2, 3, 4, 5 ]
When to Use Map vs Object
Use a plain object when you have a small, fixed set of string keys and you need JSON serialization or simple property access. Use a Map when: keys are not strings (objects, numbers), you frequently add/remove key-value pairs, you need key order preservation, or you need to iterate over entries easily. Maps also have a size property (objects don't have a built-in size).
Production Insight
In a production analytics pipeline, a team used a plain object to store thousands of session IDs as keys. Insertion and lookup O(1) degraded as V8 optimized the object's hidden class — but more importantly, iterating with for...in included prototype properties. Switching to Map fixed both issues and gave predictable performance.
Rule: For dynamic collections with frequent changes, prefer Map and Set over objects and arrays.
Key Takeaway
Map accepts any key type and preserves insertion order
Set automatically deduplicates values; .has() is O(1)
WeakMap/WeakSet for memory-sensitive caching — keys can be garbage collected

New String and Array Built-in Methods Reference Table

ES6+ added many practical methods to String and Array prototypes. Here's a quick reference for the most commonly used ones:

MethodCategoryDescriptionExample
String.prototype.includes()StringReturns true if string contains substring'Hello'.includes('ell')true
String.prototype.startsWith()StringChecks if string starts with substring'file.js'.startsWith('file')true
String.prototype.endsWith()StringChecks if string ends with substring'file.js'.endsWith('.js')true
String.prototype.repeat()StringReturns new string repeated N times'ha'.repeat(3)'hahaha'
Array.prototype.find()ArrayReturns first element that passes a test[5,12,8,130].find(x => x > 10)12
Array.prototype.findIndex()ArrayReturns index of first passing element[5,12,8,130].findIndex(x => x > 10)1
Array.prototype.fill()ArrayFills elements with a static value[1,2,3].fill(0, 0, 2)[0, 0, 3]
Array.prototype.includes()ArrayChecks if array contains a value[1,2,3].includes(2)true
Array.prototype.keys()ArrayReturns iterator of indices[...['a','b'].keys()][0,1]
Array.prototype.values()ArrayReturns iterator of values[...['a','b'].values()]['a','b']
Array.prototype.entries()ArrayReturns iterator of [index, value] pairs[...['x','y'].entries()][[0,'x'],[1,'y']]
Array.prototype.flat()ES2019Flattens nested arrays to specified depth[1,[2,[3]]].flat(2)[1,2,3]
Array.prototype.flatMap()ES2019Maps then flattens result by one level[1,2].flatMap(x => [x, x*10])[1,10,2,20]

Use these methods instead of manual loops for cleaner, more declarative code. They are widely supported in modern environments and polyfills exist for legacy browsers.

stringArrayMethods.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// ----- STRING METHODS -----
const filename = 'photo_2024.jpg';
console.log(filename.startsWith('photo')); // true
console.log(filename.endsWith('.jpg'));    // true
console.log(filename.includes('2024'));    // true

const separator = '---'.repeat(3);
console.log(separator); // '---------' (9 dashes)

// ----- ARRAY METHODS -----
const temperatures = [72, 85, 68, 90, 73];

// find first temperature over 80
const firstHot = temperatures.find(t => t > 80);
console.log('First hot day:', firstHot); // 85

// find index of first over 85
const extremeIndex = temperatures.findIndex(t => t > 85);
console.log('First extreme index:', extremeIndex); // 3

// check if any temperature is 90
console.log(temperatures.includes(90)); // true

// fill with defaults (replace elements from index 1 to 3)
const defaultTemps = [0, 0, 0, 0, 0];
defaultTemps.fill(72, 1, 3);
console.log(defaultTemps); // [0, 72, 72, 0, 0]

// keys, values, entries
const colors = ['red', 'green', 'blue'];
for (const index of colors.keys()) {
  console.log(index); // 0,1,2
}
for (const value of colors.values()) {
  console.log(value); // 'red','green','blue'
}
for (const [i, v] of colors.entries()) {
  console.log(`${i}: ${v}`);
}
// 0: red, 1: green, 2: blue

// ----- FLAT & FLATMAP (ES2019) -----
const nested = [1, [2, [3, [4]]]];
console.log(nested.flat(2)); // [1, 2, 3, [4]]

const phrases = ['hello world', 'foo bar'];
const words = phrases.flatMap(phrase => phrase.split(' '));
console.log(words); // ['hello', 'world', 'foo', 'bar']
Output
true
true
true
---------
First hot day: 85
First extreme index: 3
true
[ 0, 72, 72, 0, 0 ]
0
1
2
red
green
blue
0: red
1: green
2: blue
[ 1, 2, 3, [ 4 ] ]
[ 'hello', 'world', 'foo', 'bar' ]
Browser Compatibility Note
All methods listed above are supported in modern browsers (Chrome 45+, Firefox 25+, Safari 9+, Edge 12+). For flat and flatMap, support starts from Chrome 69, Firefox 62, Safari 12, Node 11. Use polyfills (like core-js) if targeting older environments.
Production Insight
Switching to Array.includes() instead of indexOf() !== -1 improves readability and eliminates a common source of boolean confusion. Similarly, findIndex() is clearer than manually looping to find an index. These methods are well-optimized in modern V8; no performance penalty.
Rule: Prefer .includes(), .find(), and .findIndex() over manual loops or indexOf for clarity.
Key Takeaway
Use .includes() for substring/value checks, .find()/findIndex() for conditional lookups
These methods make code more declarative and less error-prone than manual loops
flat() and flatMap() simplify nested array transformations

ES5 vs ES6 Comparison Table for Each Major Feature

Here's a side-by-side reference of how the most important features changed from ES5 to ES6+. Use this table as a quick reminder when refactoring or reviewing code.

FeatureES5 ApproachES6+ Approach
Variable declarationvar (function scope, hoisting)let / const (block scope, TDZ)
Function syntaxfunction() {} everywhereArrow functions () => {} for callbacks; regular function for methods
this in callbacksvar self = this; or .bind(this)Arrow functions inherit this lexically
String concatenation'Hello ' + name + '!';Template literals: ` Hello ${name}! `
Extracting valuesvar name = obj.name; var age = obj.age;Destructuring: const { name, age } = obj;
Copying/merging objectsObject.assign({}, obj)Spread: { ...obj }
Copying/merging arraysArray.prototype.concat() or slice()Spread: [...arr]
Variable number of argsarguments object (array-like)Rest parameters: ...args (real array)
Async codeNested callbacks (callback hell)Promises + async/await (linear flow)
Module system<script> tags, IIFEs, globalsimport / export (static, tree-shakable)
Iterating arraysfor (var i=0; i<arr.length; i++) or arr.forEach()for...of loop (values, break/continue)
Data structuresPlain objects and arraysMap, Set, WeakMap, WeakSet
String methodsindexOf() for substring check.includes(), .startsWith(), .endsWith()
Array methodsManual loops or indexOf.find(), .findIndex(), .includes(), .flat(), .flatMap()

This table doesn't cover every change, but it captures the most impactful shifts that you'll encounter daily.

es5vsEs6Examples.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// QUICK COMPARISON SNIPPETS:

// 1. Variable scoping
// ES5
var x = 1;
if (true) { var x = 2; } // x is overwritten to 2
// ES6
let y = 1;
if (true) { let y = 2; } // y remains 1 outside

// 2. Function and this
// ES5
var obj = {
  name: 'Alice',
  greet: function() {
    var self = this;
    setTimeout(function() { console.log('Hi ' + self.name); }, 100);
  }
};
// ES6
var obj = {
  name: 'Alice',
  greet: function() {
    setTimeout(() => console.log(`Hi ${this.name}`), 100);
  }
};

// 3. String interpolation
// ES5: 'Welcome ' + user.name + ', you have ' + count + ' items.'
// ES6: `Welcome ${user.name}, you have ${count} items.`

// 4. Extracting values
// ES5: var name = user.name; var role = user.role;
// ES6: const { name, role } = user;

// 5. Copying arrays
// ES5: var copy = original.slice();
// ES6: const copy = [...original];
Output
(No output — these are patterns, not runnable snippets; see the code files above for executable examples.)
Use This Table as a Refactoring Checklist
When modernizing an older codebase, go through each row: replace var with const/let, convert callback nesting to Promises/async/await, switch string concatenation to template literals, and introduce destructuring and spread. The table gives you a clear 'before and after' for each change.
Production Insight
Teams that systematically apply ES6+ refactoring patterns see a reduction in 'this' bugs and callback-related errors. The upfront time investment pays off quickly — less time debugging scope issues and more time building features.
Rule: When in doubt, the right side of the table is almost always the better choice for new code.
Key Takeaway
ES6+ syntax is not just syntactic sugar — it eliminates entire categories of bugs
Always prefer the ES6+ approach unless you have a specific compatibility constraint
Use this table as a quick reference when reading or writing modern JavaScript
● Production incidentPOST-MORTEMseverity: high

Arrow Function in Event Listener Crashes Live User Profile

Symptom
Clicking 'Edit Profile' opened the form but submitting did nothing. No error visible to user, but console error pointed to this.submit being undefined.
Assumption
The developer assumed arrow functions behave identically to regular functions in event handlers. They wrote button.addEventListener('click', () => this.submitForm()) inside a class component.
Root cause
Arrow functions do not bind their own this; they inherit from the enclosing scope. Inside a class method, the enclosing scope is the class instance, but when the arrow function is passed as a callback to addEventListener, the this inside the arrow function still refers to the class instance (correct). However, in this case, the arrow function was defined in a constructor, but the this inside the constructor's arrow callback actually correctly refers to the instance. The bug was different: the developer used an arrow function as an object method directly: const handler = { click: () => this.submitForm() } and then passed handler.click. The arrow function's this was the global object (or undefined in strict mode) because it was not called on an object. The root cause: arrow functions are not suitable for object methods.
Fix
Changed the arrow function to a regular function: click: function() { this.submitForm(); } or use method shorthand click() { this.submitForm(); }. The event listener then worked because this was correctly bound to the object.
Key lesson
  • Never use arrow functions as object methods or when you need dynamic this binding (e.g., event listeners on DOM elements where you want this to be the element).
  • Use arrow functions for callbacks where you want to capture the surrounding this (e.g., in class methods passed to setTimeout).
  • If you see 'undefined' where an object method should be, suspect arrow function misuse first.
Production debug guideSymptoms, root causes, and actions for the three most frequent ES6+ gotchas that break live applications.3 entries
Symptom · 01
Button click or event handler gives 'this is undefined'
Fix
Check if the handler is an arrow function. Arrow functions inherit this — if used as a method on an object literal, this is the outer scope. Replace with regular function or method shorthand.
Symptom · 02
Promise rejection not caught, followed by 'UnhandledPromiseRejection' warning
Fix
Add .catch() to all Promise chains or wrap await calls in try/catch. If using top-level await in Node, use process.on('unhandledRejection', handler) as fallback.
Symptom · 03
Async function returns a Promise instead of the expected value
Fix
Async functions always return a Promise. If you assign const result = asyncFunction(), you get a Promise. You must await it or use .then().
★ Quick Debug Cheat Sheet for ES6+ MistakesImmediate commands and fixes for the most common ES6+ production breakages.
`this` is undefined in object method
Immediate action
Check the method definition — look for `=>` after method name.
Commands
console.log(this) inside the method to confirm scope.
Change arrow function to method shorthand: `{ method() { ... } }`
Fix now
Replace { method: () => { ... } } with { method() { ... } } or { method: function() { ... } }
Unhandled Promise rejection in Node.js (node v15+)+
Immediate action
Add `process.on('unhandledRejection', (reason, promise) => { console.error(reason); process.exit(1); })` at entry point.
Commands
grep for .catch or try/catch in the failing route handler.
Run with `--unhandled-rejections=strict` flag to break at the rejection.
Fix now
Wrap the async function call in try/catch: try { await riskyFunction(); } catch (e) { handleError(e); }
Spread operator modifies original object unexpectedly+
Immediate action
Check if the object has nested objects. Spread creates a shallow copy.
Commands
console.log(JSON.stringify(original, null, 2)) and JSON.stringify(copy, null, 2) to compare.
Use `structuredClone(original)` or `JSON.parse(JSON.stringify(original))` for deep copy.
Fix now
For production, use structuredClone() with a fallback for older environments: const clone = structuredClone ? structuredClone(obj) : JSON.parse(JSON.stringify(obj));
FeatureOld Approach (ES5)ES6+ Approach
Variable declarationvar (function-scoped, hoisted)let / const (block-scoped, predictable)
Function syntaxfunction keyword everywhereArrow functions for callbacks; regular functions for methods
String building"Hello " + name + ", you have " + count + " messages"Hello ${name}, you have ${count} messages
Extracting object valuesvar name = user.name; var age = user.age;const { name, age } = user;
Combining arrays/objectsArray.prototype.concat(), Object.assign()Spread operator: [...arr], {...obj}
Async codeNested callbacks (callback hell)Promises + async/await (reads top-to-bottom)
Variable number of argsarguments object (array-like, not real array)Rest parameters: ...args (a real array)
this in callbacksvar self = this; or .bind(this)Arrow functions inherit this lexically
Module system<script> tags, IIFEs, global variablesimport / export (static, tree-shakable)

Key takeaways

1
Use const by default and let only when reassignment is genuinely needed
var should never appear in code written after 2015.
2
Arrow functions solve the this binding problem in callbacks, but they are not a universal replacement for function
never use them as object/class methods.
3
Spread (...) creates shallow copies only
nested objects are still shared references, which will surprise you the first time you mutate a 'copy' and see the original change too.
4
async/await is Promises with better syntax
you must still handle rejections with try/catch, and sequential await inside a loop is a performance trap that Promise.all solves.
5
ES6 modules (import/export) give you real scoping and eliminate global namespace pollution
adopt them in every project to make dependencies explicit and tree-shakeable.

Common mistakes to avoid

6 patterns
×

Using arrow function as object method

Symptom
this is undefined inside the method. Method call returns Cannot read property '...' of undefined.
Fix
Replace arrow function with regular function or method shorthand: { method() { ... } } instead of { method: () => { ... } }.
×

Assuming `const` makes objects immutable

Symptom
Properties of a const object change unexpectedly, and developer thinks the variable itself is frozen. No error thrown — mutation silently succeeds.
Fix
Use Object.freeze() for shallow immutability, or Object.freeze() recursively for deep freeze. Use structuredClone() for deep copying.
×

Forgetting `await` inside async function

Symptom
The function returns a Promise instead of the expected resolved value. Code that uses the result sees a Promise object, not the actual data.
Fix
Always use await when calling an async function inside another async function. Or use .then() if not in async context.
×

Using `await` in a loop sequentially when requests are independent

Symptom
Performance is much slower than expected. Each request waits for the previous, increasing total time linearly with number of requests.
Fix
Replace sequential await with Promise.all(): const results = await Promise.all(tasks.map(task => fetch(task)))
×

Confusing rest parameters with the `arguments` object

Symptom
Using arguments in an arrow function throws ReferenceError because arrow functions don't have arguments. Or using arguments where rest parameters would be cleaner.
Fix
Always use rest parameters (...args) when you need a variable number of arguments. Rest parameters are a real array. Add 'use strict' only if needed - modern modules are strict by default.
×

Not understanding that spread creates shallow copies

Symptom
Modifying a nested property in a spread copy unexpectedly modifies the original object.
Fix
For deep copies, use structuredClone() (modern browsers/Node 17+) or JSON.parse(JSON.stringify(obj)) (with caveats). For one-level objects, spread is fine.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What is the difference between `let`, `const`, and `var`? Can you descri...
Q02SENIOR
Explain why you can't use an arrow function as a constructor (i.e., with...
Q03SENIOR
What is the difference between `Promise.all()` and `Promise.allSettled()...
Q01 of 03JUNIOR

What is the difference between `let`, `const`, and `var`? Can you describe a bug that `var` can cause that `let` would prevent?

ANSWER
var is function-scoped and hoisted, meaning it can be accessed before declaration (value is undefined). let and const are block-scoped and not initialized before declaration (Temporal Dead Zone). const additionally prevents reassignment of the binding, but not mutation of the value. Example bug with var: In a for loop, if you create closures inside the loop, each closure references the same var variable (due to hoisting), so after the loop, all closures see the final value. With let, each iteration creates a new binding, so closures work correctly. ``javascript for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i), 100); // prints 3,3,3 } for (let i = 0; i < 3; i++) { setTimeout(() => console.log(i), 100); // prints 0,1,2 } ``
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
Do I need to know ES6 for React or Node.js development?
02
What is the difference between `==` and `===` in JavaScript, and is that an ES6 thing?
03
Is there a performance difference between arrow functions and regular functions?
04
How do I convert a legacy codebase to use ES6+ features?
05
What's the difference between `import { foo }` and `import foo`?
🔥

That's Advanced JS. Mark it forged?

6 min read · try the examples if you haven't

Previous
Prototypes and Inheritance in JS
6 / 27 · Advanced JS
Next
Destructuring in JavaScript