Dev.to WebDev 🛠 Dev 👁 1 📖 4 min read

Mastering Modern JavaScript: From ES6 to Async Patterns

Introduction JavaScript has evolved dramatically since the early days of alert('Hello World'). Today's JavaScript powers everything from tiny widgets to massive server‑side applications, thanks to ECMAScript 6 (ES6) an

Introduction

JavaScript has evolved dramatically since the early days of alert('Hello World'). Today's JavaScript powers everything from tiny widgets to massive server‑side applications, thanks to ECMAScript 6 (ES6) and the ecosystem that grew around it. In this post we’ll walk through the most impactful language features introduced in the last decade, show how they simplify real‑world code, and explore async patterns that every modern developer should master.

1. Let, Const, and Block Scope

Before ES6, var was the only way to declare variables, leading to hoisting surprises.

function legacyScope() {
  var i = 0;
  for (var i = 0; i < 3; i++) {
    // i is function‑scoped, not block‑scoped
  }
  console.log(i); // 3, not 0
}

let and const fix that:

function blockScope() {
  const arr = [];
  for (let i = 0; i < 3; i++) {
    arr.push(i);
  }
  // console.log(i); // ReferenceError: i is not defined
  return arr;
}
  • let – mutable, block‑scoped.
  • const – immutable binding, block‑scoped. The value can still be mutable if it’s an object.

2. Arrow Functions and Lexical this

Arrow functions give you a concise syntax and capture the surrounding this value.

// Traditional callback
setTimeout(function() {
  console.log(this); // Window (or undefined in strict mode)
}, 1000);

// Arrow version – inherits lexical this
class Timer {
  constructor() {
    this.start = Date.now();
  }
  log() {
    setTimeout(() => {
      console.log('Elapsed:', Date.now() - this.start);
    }, 1000);
  }
}
new Timer().log();

Because the arrow function does not create its own this, you avoid the common var self = this pattern.

3. Template Literals

String interpolation, multi‑line strings, and embedded expressions are now a breeze.

const user = { name: 'Ada', points: 42 };
const message = `
  Hello ${user.name}!
  You have earned ${user.points} points.
  Keep it up!
`;
console.log(message);

No more cumbersome concatenation or \n escapes.

4. Destructuring & Rest/Spread

Pull values out of arrays or objects directly in the declaration.

// Array destructuring
const [first, second, ...rest] = [10, 20, 30, 40];
// Object destructuring with renaming
const { title: postTitle, author = 'Anonymous' } = { title: 'JS Tips' };

Rest (...rest) gathers the remaining items, while spread (...obj) expands them:

const defaults = { theme: 'light', layout: 'grid' };
const userPrefs = { layout: 'list' };
const config = { ...defaults, ...userPrefs }; // { theme: 'light', layout: 'list' }

5. Modules (ESM)

Node.js and browsers now support native ES modules (import/export).

// utils.mjs
export function sum(a, b) {
  return a + b;
}
export const PI = Math.PI;
// app.mjs
import { sum, PI } from './utils.mjs';
console.log(`Area of circle: ${PI * sum(3, 4)}`);

No more bundler‑only require/module.exports—you can write clean, tree‑shakable code directly.

6. Classes & Inheritance

Classes are syntactic sugar over prototypal inheritance, making OOP feel familiar.

class Shape {
  constructor(name) {
    this.name = name;
  }
  area() {
    throw new Error('area() must be implemented');
  }
}

class Circle extends Shape {
  constructor(radius) {
    super('circle');
    this.radius = radius;
  }
  area() {
    return Math.PI * this.radius ** 2;
  }
}

const c = new Circle(5);
console.log(`${c.name} area = ${c.area().toFixed(2)}`);

Static methods, getters/setters, and private fields (#value) are also part of the class spec.

7. Async/Await – Writing Asynchronous Code Like Synchronous Code

Promises turned callback hell into manageable chains, but async/await makes the flow even clearer.

async function fetchUser(id) {
  const response = await fetch(`https://api.example.com/users/${id}`);
  if (!response.ok) throw new Error('Network error');
  const user = await response.json();
  return user;
}

(async () => {
  try {
    const alice = await fetchUser(1);
    console.log('User:', alice);
  } catch (e) {
    console.error(e);
  }
})();

7.1 Parallel Execution with Promise.all

When tasks are independent, run them concurrently:

async function loadDashboard() {
  const [stats, recent, alerts] = await Promise.all([
    fetch('/api/stats').then(r => r.json()),
    fetch('/api/recent').then(r => r.json()),
    fetch('/api/alerts').then(r => r.json())
  ]);
  return { stats, recent, alerts };
}

7.2 Cancelable Fetches (AbortController)

Modern APIs let you abort long‑running requests:

async function fetchWithTimeout(url, ms) {
  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), ms);
  try {
    const resp = await fetch(url, { signal: controller.signal });
    return await resp.json();
  } finally {
    clearTimeout(timeout);
  }
}

8. Optional Chaining & Nullish Coalescing

Safely access deep properties without repetitive guards.

const config = { db: { host: 'localhost' } };
const port = config.db?.port ?? 5432; // 5432 if undefined or null

The ?. operator short‑circuits when a value is null/undefined, while ?? only falls back on nullish values (not 0 or '').

9. TypeScript Integration (A Quick Note)

While this post focuses on vanilla JavaScript, most production teams pair it with TypeScript for static typing. The features we covered today map one‑to‑one in TS, giving you compile‑time safety without sacrificing runtime performance.

10. Putting It All Together – A Real‑World Mini‑Project

Let’s build a tiny Todo API client using the modern features we’ve discussed.

// todoClient.mjs
export class TodoClient {
  #baseUrl;
  constructor(baseUrl) {
    this.#baseUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
  }

  async #request(path, options = {}) {
    const url = `${this.#baseUrl}${path}`;
    const resp = await fetch(url, { headers: { 'Content-Type': 'application/json' }, ...options });
    if (!resp.ok) {
      const err = await resp.text();
      throw new Error(`HTTP ${resp.status}: ${err}`);
    }
    return resp.json();
  }

  getAll() {
    return this.#request('/todos');
  }

  add(todo) {
    return this.#request('/todos', { method: 'POST', body: JSON.stringify(todo) });
  }

  delete(id) {
    return this.#request(`/todos/${id}`, { method: 'DELETE' });
  }
}
// usage.mjs
import { TodoClient } from './todoClient.mjs';

(async () => {
  const api = new TodoClient('https://jsonplaceholder.typicode.com');
  const todos = await api.getAll();
  console.log('First 3 todos:', todos.slice(0, 3));

  const newTodo = await api.add({ userId: 1, title: 'Learn async/await', completed: false });
  console.log('Created:', newTodo);

  await api.delete(newTodo.id);
  console.log('Deleted newly created todo');
})();

Notice:

  • Private fields (#baseUrl, #request).
  • async/await for clear flow.
  • Spread operator for request options.
  • Template literals for URL construction.
  • export/import for modularity.

Conclusion

JavaScript’s rapid evolution has turned it from a quirky browser scripting language into a robust, full‑stack powerhouse. Mastering let/const, arrow functions, destructuring, modules, and async/await equips you to write cleaner, more maintainable code today. Keep experimenting with the newest proposals (e.g., Array.prototype.at, Temporal) and remember that the language’s flexibility is both its strength and its responsibility—use the right tool for the right job.

Happy coding! 🚀

📰 Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.