JavaScript Most Used Modern Features

profile picture

Janishar Ali

•

29 May 2025

JavaScript Most Used Modern Features

JavaScript has evolved significantly over the years. ES6 (2015) was a major milestone which introduced important features that transformed the way we write code in JavaScript. Since then, many successive releases have continued to add new features. In this article I will share the features which I use most frequently in both my frontend and backend projects.

String Template

It has replaced the string concatenation using + operator. It is also very convenient for composing dynamic text as well as defining multiline strings.

`some text ${variable1} ${variable2} some more text`

Example:

const user = {
firstName: 'Janishar',
lastName: 'Ali',
}

Without String Template

const message = 'Thank you ' + user.firstName + ' ' + user.lastName + ' for reading this article';
console.log(message);

// console log
// Thank you Janishar Ali for reading this article

With String Template

const message = `Thank you ${user.firstName} ${user.lastName} for reading this article`;
console.log(message);

// console log
// Thank you Janishar Ali for reading this article

Multiline Text

const message = `// source code
function fn(value){
return value++;
}
`;
console.log(message);

/*
console log
// source code
function fn(value){
return value++;
}
*/

Variable Declaration

var was the original method to declare variables but it had some side effects.

[1] var could be reassigned

var message = "Hello";
var message = "World!";
console.log(message);
// console log
// World!

[2] var was not block scoped but function scoped

function fn() {
for (var i = 0; i < 2; i++) {
var counter = i;
console.log(i);
}
console.log("counter", counter);
}

fn();

// console log
// 0
// 1
// counter 1

Here counter still exists after the for loop

Now, we can declare variables using `let` keyword.

[1] let will allow the values to be assigned but not redeclare.

let message = "Hello";
let message = "World!";
console.log(message);

// console error
// Uncaught SyntaxError: Identifier 'message' has already been declared
let message = "Hello";
message = "World!"; // allows reassignment
console.log(message); // World!

[2] let is block scoped and is not accessible outside the block, unlike var.

function fn() {
for (let i = 0; i < 2; i++) {
let counter = i;
console.log(i);
}
console.log("counter", counter);
}

fn();
// console error
// Uncaught ReferenceError: counter is not defined
// at fn (<anonymous>:6:26)
// at <anonymous>:9:1

Now, we also have const keyword for declaring variables which once declared cannot be reassigned. Similar to let it is also block scoped. It is perhaps the most used keyword for me. The modern JS development favors immutability - meaning we don't change the values but rather create new ones.

const message = "Hello";
message = "World!";
console.log(message);
// console error
// Uncaught TypeError: Assignment to constant variable.

Destructuring Assignment

[1] Arrays

We can also assign the values of an array into variables in a single statement.

const names = ["Janishar", "Ali"];
const [janishar, ali] = names;
console.log(janishar, ali);
// console log
// Janishar Ali

It is very useful in React applications.

const [expanded, setExpanded] = useState(true);

[2] Objects

We can create the variables from the object keys in a single statement. The variable names must match the keys from the object.

const user = {
firstName: 'Janishar',
lastName: 'Ali',
address: 'fifocode',
}
const {firstName, lastName} = user;
console.log(firstName, lastName);
// console log
// Janishar Ali

This is also very useful in React applications.

const { isLoggedIn, isForcedLogout, askLogin, user } = state.auth;

[3] Spread object with rest

We can also get few properties of an object and combine rest into another object.

const user = {
firstName: "Janishar",
lastName: "Ali",
address: "fifocode",
};
const { firstName, ...rest } = user;
console.log(rest);
// console log
// {lastName: 'Ali', address: 'fifocode'}

[4] Object in a function call

We can also destructure an object in a function's parameter.

const user = {
firstName: 'Janishar',
lastName: 'Ali',
address: 'fifocode',
}

function fn({firstName, lastName}){
console.log(firstName, lastName);
}

fn(user);
// console log
// Janishar Ali

This is one of the most used features in React applications to receive props in a component.

export default function ArticlePage({ article, slug }) {
...
}

Default Parameter in Functions

We can specify the default values to function parameters. These default values will be used when arguments are not passed.

function fn(name = "Name", rollNum = 0) {
console.log(name, rollNum);
}

fn();
// console log
// Name 0

fn('Janishar Ali');
// console log
// Janishar Ali 0

fn('Janishar Ali', 100);
// console log
// Janishar Ali 100

Passing variable arguments to a function

We can pass a list of arguments to a function in a very convenient way.

function fn(type, ...payload){
console.log(type);
console.log(payload);
}

fn('Message', 'Janishar', 'Ali')

// console log
// Message
// ['Janishar', 'Ali']

Spread Operator

[1] Arrays

We have a very convenient way to copy all the array elements into another array or create a new array with values from existing arrays. This is very useful for writing immutable code, especially when updating React state.

const fruits = ['Apple', 'Mango', 'Orange'];
const vegetables = ['Potato', 'Spinach'];
const basket = [...fruits, ...vegetables];
console.log(basket);
// console log
// ['Apple', 'Mango', 'Orange', 'Potato', 'Spinach']

[2] Object

Similar to an array, objects can also spread into another object. This is a very important feature to copy an object and alter only the selected fields.

const state = {
fullscreen: false,
isLoggedIn: false,
}

const newState = {
...state,
isLoggedIn: true,
}

console.log(newState);
// console log
// {fullscreen: false, isLoggedIn: true}

This feature is extremely important in React applications for mutating a component state.

const defaultFormState = {
email: "",
password: "",
isEmailError: false,
emailError: "",
isPasswordError: false,
passwordError: "",
};

const [credentials, setCredentials] = useState(defaultFormState);

const handleCredentialChange = (name) => (e) => {
e.preventDefault();
setCredentials({
...credentials,
isEmailError: false,
emailError: "",
isPasswordError: false,
passwordError: "",
[name]: e.target.value,
});
};

Arrow Operator for Function

We can use => operator to define a function as an alternative to function keyword. It's quite convenient for a functional programming.

[1] Normal Function vs Arrow Function

// normal function
function fn(type, message) {
console.log(type, message);
}
fn("transaction", "successful");
// console.log
// transaction successful
// arrow function
const fn = (type, message) => {
console.log(type, message)
}

fn('transaction', 'successful');
// console.log
// transaction successful

[2] Callback Functions

function callback(message) {
console.log(message);
}

// normal function
function fn(payload, callback) {
callback(`${payload} received`);
}

fn("payment", callback);
// console log
// payment received
function fn(payload, callback) {
callback(`${payload} received`);
}

// callback as an arrow function
fn("payment", (message) => {
console.log(message);
});
// console log
// payment received

Iterate over an Array

We can conveniently use of operator for an array iteration in place of index based iteration.

Normal Iteration

const fruits = ['Apple', 'Mango', 'Orange'];

for(let i = 0; i < fruits.length; i++){
console.log(fruits[i]);
}
// console log
// Apple
// Mango
// Orange

Iteration with of operator

const fruits = ['Apple', 'Mango', 'Orange'];

for(const fruit of fruits){
console.log(fruit);
}
// console log
// Apple
// Mango
// Orange

Array Functions

We have many utility functions associated with an Array.

[1] map

Iterates over an array and transform the items using a callback function. It produces a new array with transformed values. This is one of the most used features in React application.

const items = [
{ id: 1, name: "one" },
{ id: 2, name: "two" },
];
const names = items.map((item) => item.name);
console.log(names);
// ['one', 'two']

[2] find

Iterates over an array and returns the item matches using a callback function or undefined if no matching item is found.

const items = [
{ id: 1, name: "one" },
{ id: 2, name: "two" },
];
const found = items.find((item) => item.id == 1);
console.log(found);
// {id: 1, name: 'one'}

const notfound = items.find((item) => item.id == 3);
console.log(notfound);
// undefined

[3] filter

Iterates over an array and returns only the items which satisfy the conditions specified by the callback function.

const items = [
{ id: 1, name: "one" },
{ id: 2, name: "two" },
{ id: 3, name: "three" },
];
const filtered = items.filter((item) => item.id > 1);
console.log(filtered);
// (2) [{…}, {…}]
// 0: {id: 2, name: 'two'}
// 1: {id: 3, name: 'three'}
// length: 2

Generator Function

It is a special type of function that can pause execution and resume later, allowing us to generate values on the fly. It reduces the boiler plate to achieve a given task where state needs to be maintained.

Pagination Example

function* paginate(array, size) {
for (let i = 0; i < array.length; i += size) {
yield array.slice(i, i + size);
}
}

const pages = paginate([1, 2, 3, 4, 5, 6, 7], 3);
console.log(pages.next().value); // [1, 2, 3]
console.log(pages.next().value); // [4, 5, 6]
console.log(pages.next().value); // [7]

OOPs and Inheritance

[1] Class and Object

We can define an object's structure using class keyword. New object is created using new keyword, which calls the constructor function of the class. constructor initialises the member variables. We can access the reference to the current object using this keyword in any member functions. The class can also define methods, which are functions bound with the object. In this Example we create an Animal class and then create a cat and a dog object from that class.

class Animal {
// constructor is a special function which is called when the new keyword is used
constructor(name, sound) {
// this is a special keyword which refers to the current object
this.name = name;
this.sound = sound;
}

// member function is called methods
speak() {
// methods can access the current object using this keyword
console.log(this.name, this.sound);
}
}

const cat = new Animal("Cat", "Meow!");
cat.speak(); // Cat Meow!

const dog = new Animal("Dog", "Barks!");
dog.speak(); // Dog Barks!

[2] Inheritance

Often, we want to add properties on top of an existing class. We can extend a class using extends keyword. When we do this, we need to call the parent class's constructor using super function. We can also add more member variables to the child class. The methods of the super class is also accessible to the child class.

In the example below, Cat class inherits Animal class.

class Animal {
constructor(name, sound) {
this.name = name;
this.sound = sound;
}

speak() {
console.log(this.name, this.sound);
}
}

class Cat extends Animal {
constructor(breed) {
// super is a special function to call the parent's constructor
super("Cat", "Meow!");
// the child class can add its own properties
this.breed = breed;
}

showBreed() {
console.log(this.breed);
}
}

const cat = new Cat("Persian");
cat.speak(); // Cat Meow!
cat.showBreed(); // Persian

[3] Static Function and variables

The JS does not support static member variables directly but we can check its existence and only set it once in the constructor. We can define static members to a class which can be accessed using class name rather than an instance.

class Animal {
constructor(name, sound) {
// sets the static variable
if (Animal.count == undefined) Animal.count = 0;
Animal.count++;

this.name = name;
this.sound = sound;
}

speak() {
console.log(this.name, this.sound);
}

// defines a static function which is defined at the class level
static population() {
console.log(Animal.count);
}
}

const cat = new Animal("Cat", "Meow!");
Animal.population(); // 1
const dog = new Animal("Dog", "Barks!");
Animal.population(); // 2

Modules

We can export variables and functions from one file and import them inside another using export and import keywords.

Note: This is only supported with type="module" in scripts inside a html page. In NodeJs we can enable module using "type": "module" in package.json
// module1.js
export const module1 = {
name: "Module 1",
};

// module2.js
import { module1 } from "./module1";

Set

We can define a collection with unique elements using Set.

const s = new Set();
s.add("Apple");
s.add("Mango");
s.add("Apple"); // duplicate

console.log(s);
// console log
// Set(2) {'Apple', 'Mango'}

Map

We can define a key-value pairs using Map and access the value by key.

const m = new Map();
m.set('key1', 'value1');
m.set('key2', 'value2');

console.log(m.get('key1'));
// console log
// value1

Optional Chaining ?.

We can access the members properties only when it is defined using ?. operator. It helps avoid many if-else statements.

Without chaining

const state = {};
console.log(state.auth.token);
// Uncaught TypeError: Cannot read properties of undefined (reading 'token')

With Chaining

const state = {}
console.log(state.auth?.token);
// undefined

Nullish Coalescing ??

Returns a default value if the access variable is undefined.

const state = {};
const token = state.auth?.token ?? "Not Present";
console.log(token);
// Not Present
const state = {
auth: {
token: "access-token",
},
};
const token = state.auth?.token ?? "Not Present";
console.log(token);
// access-token

Promises

Promise is a powerful way to handle asynchronous operations. Let's see how to create a promise object. A Promise object takes a callback function with two special parameters: resolve and reject.

Note: We use async/wait in place of Promises now. But let's first discuss the original implementation before going to async/await.
function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("Data received!");
}, 3000);
});
}

fetchData().then((data) => console.log(data));
// Promise {<pending>}
// Data received!

In this above example the promise was suspended for 3 sec. After 3 sec the timer ticked and the resolve function is called. When the resolve function is called then the then function defined callback is called, which logs the data

A promise has 3 states:

  1. Pending - waiting to be rejected/resolved
  2. Fulfilled - resolve() is called
  3. Rejected - reject() is called

When we call reject then an error is thrown. We can catch this error is a catch function

function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject("Error");
}, 3000);
});
}

fetchData().then(data => console.log(data));
// Uncaught (in promise) Error

Let's catch this error

function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject("Error");
}, 3000);
});
}

fetchData()
.then(data => console.log(data))
.catch(err => console.log(err));
// Error, but the program did not crash

Finally block run after the promise is fulfilled

function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject("Error");
}, 3000);
});
}

fetchData()
.then(data => console.log(data))
.finally(() => console.log('Done!'))
// Done!
// Uncaught (in promise) Error

async/await

We can now save ourselves from writing callbacks for resolve and reject promises manually. Also, we no longer required to use then block to receive the results.

function wait(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}

async function fetchData() {
await wait(3000);
return "Data received!";
}

const data = await fetchData();
console.log(data);
// Data received!

We define a function as asynchronous using async keyword. and we wait for the async function to return by calling await on the function call.

Here, you must be wondering what is wait function and we still using Promise object there. This wait is a utility function that helps to suspend the execution on the line await inside the fetchData function, so that we can demonstrate that the execution is indeed suspended when we call await fetchData()

Catch Error from an async function

We have to use try/catch block to catch the error from the fetchData function.

function wait(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}

async function fetchData() {
await wait(3000);
throw new Error();
}

try {
const data = await fetchData();
console.log(data);
} catch (err) {
console.log(err);
}
// Error
// at fetchData (<anonymous>:7:9)
// at async <anonymous>:11:14

async/await is very powerful in handling IO operations, like network calls.

Conclusion

These were the most important JavaScript modern features that I use frequently in my code base. I hope this article provided a good overview. I will write on the use-cases where I use them in my future articles.

JAVASCRIPT
ES6
JS
LANGUAGE
WEBDEV
profile picture
Written by Janishar Ali

Share this article and spread the knowledge

Write Your Comment
JavaScript Most Used Modern Features