Skip to content

ES6+ Features

ES6 (ECMAScript 2015) and the versions that came after it introduced modern features that made JavaScript:

  • Easier to read
  • Less likely to cause errors
  • More expressive
  • Better suited for large-scale applications
graph TD A[Old JS Problems] --> B[Global Scope Issues] A --> C[Verbose Syntax] A --> D[Callback Hell] B --> E[let/const] C --> F[Arrow Functions, Destructuring] D --> G[Promises, async/await]
let count = 1;
count = 2;
const appName = "StudyVault";
  • let -> this is scoped to the block it is written in.
  • const -> this is also scoped to the block, but its value cannot be reassigned once it is set.
console.log(a); // error
let a = 10;
const name = "Asha";
const msg = `Hello ${name}`;
const multi = `
Line 1
Line 2
`;
const user = { name: "Riya", city: "Pune" };
const { name, city } = user;
const { name: username, age = 18 } = user;
const arr = [1, 2];
const newArr = [...arr, 3]; // spread
function sum(...nums) {
// rest
return nums.reduce((a, b) => a + b, 0);
}

The spread operator is used to expand (spread out) values, while the rest operator is used to collect (gather) values together.

graph LR A[Spread] --> B[Expand Values] C[Rest] --> D[Collect Values]
function greet(name = "Guest") {
return `Hello ${name}`;
}
function test(a = b, b = 2) {}
// error (TDZ)

This causes an error because of the Temporal Dead Zone (TDZ), b is being used as a default for a before b itself has actually been set up yet.

const add = (a, b) => a + b;

No this binding

It uses lexical this, meaning it borrows this from the surrounding code instead.

No arguments object

Use the rest operator (...) instead, since arrow functions do not have their own arguments object.

Cannot be constructor

You cannot use new with an arrow function to create objects.

const obj = {
value: 10,
fn: () => console.log(this.value),
};
obj.fn(); // undefined
const name = "Asha";
const user = {
name,
greet() {
return `Hi ${this.name}`;
},
};
// old way
const { add } = require("./math.js");
// export
export function add(a, b) {
return a + b;
}
// import
import { add } from "./math.js";
  • Named export
  • Default export
export default function () {}
graph LR A[Module A] --> B[Exports] B --> C[Module B Imports]
class User {
constructor(name) {
this.name = name;
}
greet() {
return `Hi ${this.name}`;
}
}
Promise.all([p1, p2]);
Promise.race([p1, p2]);
Promise.allSettled([p1, p2]);

all

waits for every single promise to finish.

race

only waits for whichever promise finishes first, whether it succeeds or fails.

allSettled

waits for all of them to finish, without failing early if one of them fails.

user?.profile?.email;

This safely checks each step along the way, so if user or profile happens to be missing, it simply returns undefined instead of throwing an error.

const value = input ?? "default";
0 || "default"; // "default"
0 ?? "default"; // 0

|| treats any falsy value (like 0, "", or false) as a reason to use the fallback. ?? only falls back when the value is actually null or undefined, which is why 0 ?? "default" keeps the 0.

graph TD A[ES6+] --> B[Syntax] A --> C[Async] A --> D[Modules] B --> E[let const destructuring] C --> F[promises async await] D --> G[import export]
  1. Use const by default, unless you know you need to reassign the variable.
  2. Use destructuring carefully, so your code stays easy to follow.
  3. Prefer arrow functions for callbacks.
  4. Use modules to keep your code organized.
  5. Avoid overusing short, clever syntax just for the sake of it.
try {
const response = await fetch("/api/data");
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
console.log(data);
} catch (error) {
console.error("Request failed:", error.message);
}