Master modern JavaScript with ES6 and beyond. Learn essential features that will make your code cleaner, more efficient, and easier to maintain.
ES6 (ECMAScript 2015) introduced significant improvements to JavaScript, and each year brings new features. These modern features help you write cleaner, more readable, and more efficient code.
This guide covers the most important and commonly used ES6+ features that every JavaScript developer should know.
Block-scoped variable declarations that replace var. Use const for values that won't be reassigned, and let for variables that will change.
var name = 'John';
var age = 30;
age = 31; // Can change
const name = 'John'; // Won't change
let age = 30;
age = 31; // Can change
const by default for all variableslet only when you need to reassignvar in modern JavaScript
A shorter syntax for writing functions with implicit returns and lexical this binding. Perfect for callbacks and functional programming.
const add = function(a, b) {
return a + b;
};
array.map(function(item) {
return item * 2;
});
const add = (a, b) => a + b;
array.map(item => item * 2);
// Single parameter (parentheses optional)
const double = x => x * 2;
// Multiple parameters
const sum = (a, b, c) => a + b + c;
// Multi-line body (requires return)
const processData = (data) => {
const result = data.map(x => x * 2);
return result.filter(x => x > 10);
};
// Returning object (wrap in parentheses)
const makePerson = (name, age) => ({ name, age });
thisString interpolation and multi-line strings using backticks. Embed expressions directly in strings without concatenation.
var name = 'John';
var age = 30;
var message = 'Hello, ' + name +
'! You are ' + age + ' years old.';
const name = 'John';
const age = 30;
const message = `Hello, ${name}!
You are ${age} years old.`;
// Expression evaluation
const price = 19.99;
const tax = 0.08;
const total = `Total: $${(price * (1 + tax)).toFixed(2)}`;
// Multi-line strings
const html = `
<div class="card">
<h2>${title}</h2>
<p>${description}</p>
</div>
`;
// Tagged templates
const highlight = (strings, ...values) => {
return strings.reduce((result, str, i) =>
`${result}${str}<mark>${values[i] || ''}</mark>`, '');
};
Extract values from arrays or objects into distinct variables. Makes code more readable and eliminates repetitive property access.
// Basic destructuring
const person = { name: 'John', age: 30, city: 'NYC' };
const { name, age, city } = person;
// With default values
const { name, country = 'USA' } = person;
// Renaming variables
const { name: fullName, age: years } = person;
// Nested destructuring
const user = {
id: 1,
profile: { name: 'John', email: 'john@example.com' }
};
const { profile: { name, email } } = user;
// Basic destructuring
const colors = ['red', 'green', 'blue'];
const [first, second, third] = colors;
// Skip elements
const [primary, , tertiary] = colors;
// Rest pattern
const [head, ...tail] = colors;
// Swapping variables
let a = 1, b = 2;
[a, b] = [b, a];
The spread operator (...) expands iterables, while the rest operator collects multiple elements. Same syntax, different contexts.
// Array spreading
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const combined = [...arr1, ...arr2]; // [1, 2, 3, 4, 5, 6]
// Object spreading
const defaults = { theme: 'dark', fontSize: 14 };
const userPrefs = { fontSize: 16, language: 'en' };
const config = { ...defaults, ...userPrefs };
// { theme: 'dark', fontSize: 16, language: 'en' }
// Copying arrays/objects
const arrCopy = [...arr1];
const objCopy = { ...defaults };
// Function rest parameters
function sum(...numbers) {
return numbers.reduce((total, n) => total + n, 0);
}
sum(1, 2, 3, 4); // 10
// Destructuring with rest
const [first, ...rest] = [1, 2, 3, 4];
// first = 1, rest = [2, 3, 4]
const { name, ...otherProps } = { name: 'John', age: 30, city: 'NYC' };
// name = 'John', otherProps = { age: 30, city: 'NYC' }
Represent asynchronous operations, providing a cleaner alternative to callbacks. Promises have three states: pending, fulfilled, or rejected.
// Creating a Promise
const fetchData = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
const success = true;
if (success) {
resolve({ data: 'User data' });
} else {
reject('Failed to fetch');
}
}, 1000);
});
};
// Using Promises
fetchData()
.then(result => console.log(result.data))
.catch(error => console.error(error))
.finally(() => console.log('Done'));
// Promise chaining
fetchUser()
.then(user => fetchPosts(user.id))
.then(posts => displayPosts(posts))
.catch(error => handleError(error));
// Promise.all - Wait for all
Promise.all([promise1, promise2, promise3])
.then(results => console.log(results));
// Promise.race - First to resolve
Promise.race([promise1, promise2])
.then(result => console.log(result));
// Promise.allSettled - All results (ES2020)
Promise.allSettled([promise1, promise2])
.then(results => console.log(results));
Syntactic sugar over Promises that makes asynchronous code look and behave like synchronous code. Much easier to read and debug.
function getUser() {
return fetchUser()
.then(user => fetchPosts(user.id))
.then(posts => {
return { user, posts };
})
.catch(error => {
console.error(error);
});
}
async function getUser() {
try {
const user = await fetchUser();
const posts = await fetchPosts(user.id);
return { user, posts };
} catch (error) {
console.error(error);
}
}
// Parallel execution
async function fetchAll() {
const [users, posts, comments] = await Promise.all([
fetchUsers(),
fetchPosts(),
fetchComments()
]);
return { users, posts, comments };
}
// Sequential execution
async function processItems(items) {
for (const item of items) {
await processItem(item);
}
}
// Error handling
async function safeRequest() {
try {
const response = await fetch('/api/data');
const data = await response.json();
return data;
} catch (error) {
console.error('Request failed:', error);
return null;
}
}
Syntactic sugar over JavaScript's prototype-based inheritance. Provides a clearer and more familiar syntax for creating objects and handling inheritance.
// Basic class
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
return `Hello, I'm ${this.name}`;
}
// Getter
get info() {
return `${this.name} (${this.age})`;
}
// Setter
set updateAge(newAge) {
this.age = newAge;
}
// Static method
static species() {
return 'Homo sapiens';
}
}
// Inheritance
class Employee extends Person {
constructor(name, age, role) {
super(name, age);
this.role = role;
}
greet() {
return `${super.greet()}, I'm a ${this.role}`;
}
}
const dev = new Employee('John', 30, 'Developer');
console.log(dev.greet());
Split code into reusable modules with explicit imports and exports. Essential for organizing large applications and code reusability.
// Named exports
export const PI = 3.14159;
export function add(a, b) {
return a + b;
}
export class Calculator {
// ...
}
// Export list
const multiply = (a, b) => a * b;
const divide = (a, b) => a / b;
export { multiply, divide };
// Default export
export default function subtract(a, b) {
return a - b;
}
// Named imports
import { PI, add, Calculator } from './utils.js';
// Import with alias
import { multiply as mult } from './utils.js';
// Import all
import * as utils from './utils.js';
console.log(utils.PI);
// Default import
import subtract from './utils.js';
// Mixed imports
import subtract, { add, multiply } from './utils.js';
Always use const by default. Only use let when you know the variable will be reassigned. This makes your code more predictable and prevents accidental mutations.
Arrow functions are great for most cases, but avoid them for object methods that need this binding, constructors, or when you need the arguments object.
Prefer async/await over raw Promises for better readability. It makes asynchronous code look synchronous and is easier to debug with standard try/catch blocks.
Use destructuring to extract values from objects and arrays. It reduces code repetition and makes your intentions clearer to other developers.
Use template literals for any string that includes variables or spans multiple lines. They're more readable than concatenation and support embedded expressions.