JavaScript ES6 Classes Promises & Async/Await

Zaheer Ahmad 14 min read min read
Python
JavaScript ES6 Classes Promises & Async/Await

Introduction

JavaScript has come a long way since its early days. When ECMAScript 6 (ES6) was released in 2015, it transformed JavaScript from a simple scripting language into a powerful, modern programming language capable of building full-scale web applications, mobile apps, and server-side systems.

If you are a student in Pakistan learning web development — whether you are studying at a university in Lahore, taking online courses in Karachi, or self-learning from home in Islamabad — mastering ES6+ features is no longer optional. It is expected by every employer, every bootcamp, and every modern JavaScript framework like React, Vue, and Node.js.

In this tutorial, we will focus on three of the most important ES6+ features:

  • JavaScript Classes — a clean, object-oriented way to structure your code
  • JavaScript Promises — a modern approach to handling asynchronous operations
  • Async/Await — syntactic sugar that makes asynchronous code look and behave like synchronous code

By the end of this tutorial, you will understand how to write cleaner, more professional JavaScript — the kind of code that gets you hired.

Prerequisites

Before diving in, make sure you are comfortable with the following concepts. If any of these feel unfamiliar, we recommend reviewing our related tutorials first.

  • Basic JavaScript syntax — variables, functions, loops, and conditionals
  • JavaScript objects and arrays — creating, accessing, and modifying them
  • The concept of callbacks — understanding how functions can be passed as arguments
  • Basic understanding of the DOM — helpful but not strictly required
  • JavaScript this keyword — knowing how this behaves in different contexts

If you have been writing JavaScript for at least a few weeks and have built a few small projects, you are ready to proceed.


Core Concepts & Explanation

JavaScript Classes — Object-Oriented Programming Made Simple

Before ES6, JavaScript developers used constructor functions and prototypes to create reusable objects. The syntax was confusing and inconsistent. ES6 introduced the class keyword, which provides a much cleaner and more intuitive way to define objects with shared behavior.

Think of a class as a blueprint. For example, if you are building a university management system for a Pakistani institution, you might have a Student class that defines what every student object looks like — their name, roll number, and what actions they can perform (like enrolling in a course or checking their CGPA).

Here is the anatomy of a JavaScript class:

class Student {
  // The constructor runs when a new object is created
  constructor(name, rollNumber, cgpa) {
    this.name = name;
    this.rollNumber = rollNumber;
    this.cgpa = cgpa;
  }

  // A method — a function that belongs to this class
  introduce() {
    return `Hi! I am ${this.name}, Roll No: ${this.rollNumber}, CGPA: ${this.cgpa}`;
  }

  // Another method
  isEligibleForScholarship() {
    return this.cgpa >= 3.5;
  }
}

// Creating instances (objects) from the class
const student1 = new Student("Ahmad", "L1F21BSCS0042", 3.8);
const student2 = new Student("Fatima", "L1F21BSCS0099", 3.2);

console.log(student1.introduce());
// Output: Hi! I am Ahmad, Roll No: L1F21BSCS0042, CGPA: 3.8

console.log(student1.isEligibleForScholarship()); // true
console.log(student2.isEligibleForScholarship()); // false

Inheritance with extends is where classes become truly powerful. Inheritance allows one class to build upon another, reusing code without rewriting it.

class GraduateStudent extends Student {
  constructor(name, rollNumber, cgpa, researchTopic) {
    super(name, rollNumber, cgpa); // Call the parent class constructor
    this.researchTopic = researchTopic;
  }

  introduce() {
    // Override the parent method
    return `${super.introduce()} | Research: ${this.researchTopic}`;
  }
}

const grad = new GraduateStudent("Ali", "MS21CS001", 3.9, "Machine Learning in Urdu NLP");
console.log(grad.introduce());
// Output: Hi! I am Ali, Roll No: MS21CS001, CGPA: 3.9 | Research: Machine Learning in Urdu NLP

Key class concepts to remember:

  • constructor() — runs automatically when new ClassName() is called
  • this — refers to the current instance of the class
  • extends — creates a child class that inherits from a parent class
  • super() — calls the parent class's constructor or method
  • Getters and Setters — use get and set keywords to control property access

JavaScript Promises — Managing Asynchronous Operations

JavaScript is single-threaded, meaning it can only do one thing at a time. But many operations — like fetching data from an API, reading a file, or waiting for a timer — take time. We do not want our entire application to freeze while waiting.

This is where Promises come in. A Promise represents a value that may not be available yet but will be resolved in the future. Think of it like ordering biryani at a restaurant in Karachi — the waiter gives you a token (the Promise), and you can continue chatting while you wait. When the order is ready, you are notified (the Promise resolves).

A Promise can be in one of three states:

State Meaning
Pending The operation is still in progress
Fulfilled The operation completed successfully
Rejected The operation failed with an error

Here is how to create and use a Promise:

// Creating a Promise
const fetchStudentData = new Promise((resolve, reject) => {
  // Simulate fetching data from a server (takes 2 seconds)
  setTimeout(() => {
    const success = true; // Change to false to test rejection

    if (success) {
      resolve({ name: "Fatima", cgpa: 3.7, city: "Lahore" });
    } else {
      reject(new Error("Failed to fetch student data from server"));
    }
  }, 2000);
});

// Using the Promise with .then() and .catch()
fetchStudentData
  .then((studentData) => {
    console.log("Data received:", studentData);
    console.log(`Welcome, ${studentData.name} from ${studentData.city}!`);
  })
  .catch((error) => {
    console.error("Something went wrong:", error.message);
  })
  .finally(() => {
    console.log("Request complete — hiding loading spinner");
  });

Promise Chaining allows you to perform multiple asynchronous operations in sequence:

// Simulate an e-commerce flow for a Pakistani online shop
function verifyPayment(amount) {
  return new Promise((resolve) => {
    setTimeout(() => resolve({ verified: true, amount }), 1000);
  });
}

function processOrder(paymentInfo) {
  return new Promise((resolve) => {
    setTimeout(() => resolve({ orderId: "PK-2024-9871", ...paymentInfo }), 800);
  });
}

function sendConfirmation(order) {
  return new Promise((resolve) => {
    setTimeout(() => resolve(`Order ${order.orderId} confirmed! SMS sent.`), 500);
  });
}

// Chain the Promises
verifyPayment(2500) // PKR 2500
  .then((payment) => processOrder(payment))
  .then((order) => sendConfirmation(order))
  .then((message) => console.log(message))
  .catch((error) => console.error("Transaction failed:", error.message));
// Output: Order PK-2024-9871 confirmed! SMS sent.

Async/Await — Writing Asynchronous Code Like It Is Synchronous

While Promises are a huge improvement over callbacks, long chains of .then() can still be hard to read. Async/Await was introduced in ES2017 (ES8) and is built on top of Promises — it gives us a way to write asynchronous code that reads naturally, top to bottom, like regular synchronous code.

The two keywords work together:

  • async — placed before a function declaration, it makes the function automatically return a Promise
  • await — can only be used inside an async function; it pauses execution until the Promise resolves
// The same Promise chain from before, rewritten with async/await
async function processPaymentFlow(amount) {
  try {
    const payment = await verifyPayment(amount);
    console.log("Payment verified:", payment);

    const order = await processOrder(payment);
    console.log("Order processed:", order);

    const confirmation = await sendConfirmation(order);
    console.log(confirmation);

  } catch (error) {
    console.error("Payment flow failed:", error.message);
  }
}

processPaymentFlow(2500);

Notice how much cleaner this is. Each await line reads like a normal statement. Error handling is done with a familiar try/catch block rather than .catch() chained at the end.

Running Promises in Parallel with Promise.all():

Sometimes you want to run multiple async operations simultaneously rather than one after another. Promise.all() accepts an array of Promises and waits for all of them to complete:

async function loadDashboardData(studentId) {
  try {
    // These three requests fire simultaneously — much faster!
    const [profile, grades, notifications] = await Promise.all([
      fetchStudentProfile(studentId),
      fetchStudentGrades(studentId),
      fetchNotifications(studentId)
    ]);

    console.log("Profile:", profile);
    console.log("Grades:", grades);
    console.log("Notifications:", notifications);
  } catch (error) {
    // If ANY Promise rejects, the catch runs
    console.error("Failed to load dashboard:", error.message);
  }
}

Practical Code Examples

Example 1: Student Registration System Using Classes

Let us build a small but complete student registration system — something a Pakistani university portal might use. This demonstrates classes, inheritance, getters, and static methods working together.

// Base class: Person
class Person {
  #nationalId; // Private field (ES2022 syntax — # makes it private)

  constructor(name, email, nationalId) {
    this.name = name;
    this.email = email;
    this.#nationalId = nationalId; // Private — cannot be accessed from outside
  }

  // Getter — controlled read access to private field
  get maskedId() {
    // Show only last 4 digits of CNIC for privacy
    return `*******${this.#nationalId.slice(-4)}`;
  }

  greet() {
    return `Assalam-o-Alaikum! I am ${this.name}.`;
  }
}

// Child class: Student extends Person
class Student extends Person {
  #courses = []; // Private array to store enrolled courses

  constructor(name, email, nationalId, rollNumber, department) {
    super(name, email, nationalId); // Pass to parent
    this.rollNumber = rollNumber;
    this.department = department;
    this.feeStatus = "Unpaid";
  }

  // Enroll in a course
  enroll(courseName, creditHours) {
    this.#courses.push({ courseName, creditHours });
    console.log(`${this.name} enrolled in ${courseName} (${creditHours} credit hours)`);
  }

  // Get total enrolled credit hours
  get totalCreditHours() {
    return this.#courses.reduce((total, c) => total + c.creditHours, 0);
  }

  // View enrolled courses (returns a copy, not the private array itself)
  getCourses() {
    return [...this.#courses];
  }

  // Static method — called on the class itself, not an instance
  static generateRollNumber(department, year, sequence) {
    const deptCode = department.substring(0, 2).toUpperCase();
    return `${deptCode}-${year}-${String(sequence).padStart(4, "0")}`;
  }

  // Full profile summary
  getProfile() {
    return {
      name: this.name,
      email: this.email,
      cnic: this.maskedId,
      rollNumber: this.rollNumber,
      department: this.department,
      totalCreditHours: this.totalCreditHours,
      feeStatus: this.feeStatus
    };
  }
}

// --- Usage ---

// Generate a roll number using the static method
const rollNo = Student.generateRollNumber("Computer Science", 2024, 7);
console.log("Generated Roll Number:", rollNo);
// Output: Generated Roll Number: CO-2024-0007

// Create a student
const ahmad = new Student(
  "Ahmad Raza",
  "[email protected]",
  "3520112345678",
  rollNo,
  "Computer Science"
);

// Enroll in courses
ahmad.enroll("Data Structures", 3);
ahmad.enroll("Web Engineering", 3);
ahmad.enroll("Calculus II", 3);

// Check profile
console.log(ahmad.getProfile());
/*
{
  name: 'Ahmad Raza',
  email: '[email protected]',
  cnic: '*******5678',
  rollNumber: 'CO-2024-0007',
  department: 'Computer Science',
  totalCreditHours: 9,
  feeStatus: 'Unpaid'
}
*/

console.log(ahmad.greet());
// Output: Assalam-o-Alaikum! I am Ahmad Raza.

Line-by-line walkthrough of key parts:

  • #nationalId and #courses — private class fields (ES2022), inaccessible from outside the class
  • get maskedId() — a getter that computes a value instead of storing it
  • super(name, email, nationalId) — passes data up to the Person constructor
  • static generateRollNumber() — called as Student.generateRollNumber(), not on an instance
  • [...this.#courses] — the spread operator creates a shallow copy to prevent outside mutation

Example 2: Real-World Application — Pakistani Prayer Time API Fetcher

This example simulates a real-world async application: fetching Islamic prayer times for a Pakistani city and displaying them. It combines async/await, error handling, and Promise.all().

// Simulate an API response (in real apps, replace with actual fetch() calls)
function getPrayerTimes(city, date) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const prayerData = {
        Lahore: {
          date: date,
          city: "Lahore",
          times: { Fajr: "05:14", Dhuhr: "12:21", Asr: "15:49", Maghrib: "18:23", Isha: "19:47" }
        },
        Karachi: {
          date: date,
          city: "Karachi",
          times: { Fajr: "05:32", Dhuhr: "12:31", Asr: "15:58", Maghrib: "18:39", Isha: "20:01" }
        },
        Islamabad: {
          date: date,
          city: "Islamabad",
          times: { Fajr: "05:07", Dhuhr: "12:13", Asr: "15:38", Maghrib: "18:14", Isha: "19:38" }
        }
      };

      if (prayerData[city]) {
        resolve(prayerData[city]);
      } else {
        reject(new Error(`City "${city}" not found in database`));
      }
    }, 500); // Simulate network delay
  });
}

// Fetch and display prayer times for a single city
async function displayPrayerTimes(city) {
  console.log(`Fetching prayer times for ${city}...`);

  try {
    const today = new Date().toISOString().split("T")[0]; // "2024-11-15"
    const data = await getPrayerTimes(city, today);

    console.log(`\n📅 Prayer Times for ${data.city} — ${data.date}`);
    console.log("─".repeat(30));

    for (const [prayer, time] of Object.entries(data.times)) {
      console.log(`  ${prayer.padEnd(10)}: ${time}`);
    }
  } catch (error) {
    console.error(`Error: ${error.message}`);
  }
}

// Fetch prayer times for multiple cities at once (parallel)
async function displayAllCities() {
  const cities = ["Lahore", "Karachi", "Islamabad"];

  try {
    console.log("Fetching prayer times for all major cities simultaneously...\n");

    // All three requests fire at the same time
    const results = await Promise.all(
      cities.map(city => getPrayerTimes(city, "2024-11-15"))
    );

    results.forEach(data => {
      console.log(`🕌 ${data.city}: Fajr ${data.times.Fajr} | Maghrib ${data.times.Maghrib}`);
    });

  } catch (error) {
    // If even one city fails, we catch it here
    console.error("Failed to load city data:", error.message);
  }
}

// Run the examples
displayPrayerTimes("Lahore");
displayAllCities();

Common Mistakes & How to Avoid Them

Mistake 1: Using await Outside an async Function

This is the most common error beginners make. The await keyword can only be used inside a function declared with async. Using it at the top level of a regular script (outside any function) will throw a SyntaxError.

// ❌ WRONG — await used in a regular function
function loadData() {
  const data = await fetchStudentData(); // SyntaxError!
  console.log(data);
}

// ✅ CORRECT — function must be declared async
async function loadData() {
  const data = await fetchStudentData(); // Works perfectly
  console.log(data);
}

// ✅ ALSO CORRECT — async arrow function
const loadData = async () => {
  const data = await fetchStudentData();
  console.log(data);
};

Note: Modern JavaScript environments (Node.js 14.8+, modern browsers) support top-level await in ES modules (.mjs files), but for regular scripts and most beginner projects, always wrap your await calls inside an async function.

Mistake 2: Forgetting to Handle Promise Rejections

Unhandled Promise rejections can crash your Node.js application or cause silent failures in the browser. Always handle errors with either .catch() (for Promise chains) or try/catch (for async/await).

// ❌ WRONG — no error handling
async function fetchUserProfile(userId) {
  const response = await fetch(`https://api.example.com/users/${userId}`);
  const data = await response.json(); // Will crash if fetch fails!
  return data;
}

// ✅ CORRECT — always wrap in try/catch
async function fetchUserProfile(userId) {
  try {
    const response = await fetch(`https://api.example.com/users/${userId}`);

    // Also check if the HTTP response was successful
    if (!response.ok) {
      throw new Error(`Server error: ${response.status} ${response.statusText}`);
    }

    const data = await response.json();
    return data;
  } catch (error) {
    console.error("Failed to fetch profile:", error.message);
    return null; // Return a safe fallback value
  }
}

Mistake 3: Making Promises Sequential When They Could Be Parallel

A very common performance mistake is await-ing Promises one by one when they do not depend on each other. This makes your application needlessly slow.

// ❌ SLOW — each request waits for the previous one to finish
async function loadStudentDashboard(id) {
  const profile = await fetchProfile(id);    // Wait 500ms
  const grades = await fetchGrades(id);      // Wait 500ms more
  const schedule = await fetchSchedule(id);  // Wait 500ms more
  // Total: ~1500ms
}

// ✅ FAST — all requests fire simultaneously
async function loadStudentDashboard(id) {
  const [profile, grades, schedule] = await Promise.all([
    fetchProfile(id),    // These three
    fetchGrades(id),     // all fire
    fetchSchedule(id)    // at once
  ]);
  // Total: ~500ms (the time of the slowest one)
}

Use Promise.all() whenever your async operations are independent of each other.


Practice Exercises

Exercise 1: Build a Bank Account Class

Problem: Create a BankAccount class for a Pakistani bank that supports deposits, withdrawals, and balance checks. Requirements:

  • Private #balance field starting at 0
  • deposit(amount) method that adds to balance (reject negative amounts)
  • withdraw(amount) method that deducts from balance (reject if insufficient funds)
  • getBalance() method that returns the current balance in PKR
  • A static method isValidAmount(amount) that returns true if amount is a positive number

Solution:

class BankAccount {
  #balance = 0;
  #accountHolder;
  #transactions = [];

  constructor(accountHolder, initialDeposit = 0) {
    this.#accountHolder = accountHolder;
    if (initialDeposit > 0) {
      this.#balance = initialDeposit;
      this.#transactions.push({ type: "Initial Deposit", amount: initialDeposit });
    }
  }

  static isValidAmount(amount) {
    return typeof amount === "number" && amount > 0 && isFinite(amount);
  }

  deposit(amount) {
    if (!BankAccount.isValidAmount(amount)) {
      throw new Error("Invalid deposit amount. Must be a positive number.");
    }
    this.#balance += amount;
    this.#transactions.push({ type: "Deposit", amount });
    console.log(`✅ Deposited PKR ${amount.toLocaleString()}. New balance: PKR ${this.#balance.toLocaleString()}`);
  }

  withdraw(amount) {
    if (!BankAccount.isValidAmount(amount)) {
      throw new Error("Invalid withdrawal amount.");
    }
    if (amount > this.#balance) {
      throw new Error(`Insufficient funds. Balance: PKR ${this.#balance.toLocaleString()}`);
    }
    this.#balance -= amount;
    this.#transactions.push({ type: "Withdrawal", amount });
    console.log(`✅ Withdrew PKR ${amount.toLocaleString()}. New balance: PKR ${this.#balance.toLocaleString()}`);
  }

  getBalance() {
    return `PKR ${this.#balance.toLocaleString()}`;
  }

  getStatement() {
    console.log(`\n📄 Statement for: ${this.#accountHolder}`);
    this.#transactions.forEach(t => console.log(`  ${t.type}: PKR ${t.amount.toLocaleString()}`));
    console.log(`  Current Balance: ${this.getBalance()}\n`);
  }
}

// Test it
const account = new BankAccount("Fatima Khan", 50000);
account.deposit(25000);
account.withdraw(10000);
account.getStatement();

try {
  account.withdraw(200000); // This should fail
} catch (e) {
  console.error("Error:", e.message);
}

Exercise 2: Async Product Search with Error Handling

Problem: Simulate an async product search function for an online Pakistani grocery store. It should:

  • Accept a searchTerm and return matching products
  • Simulate a 1-second API delay
  • Reject if searchTerm is empty
  • Use async/await with proper try/catch in the calling code

Solution:

// Simulate a product database
const products = [
  { id: 1, name: "Basmati Rice 5kg", price: 1200, category: "Groceries" },
  { id: 2, name: "Rooh Afza 800ml", price: 350, category: "Beverages" },
  { id: 3, name: "Shan Biryani Masala", price: 180, category: "Spices" },
  { id: 4, name: "Nestle Milk Pack 1L", price: 280, category: "Dairy" },
  { id: 5, name: "Sunridge Flour 10kg", price: 1100, category: "Groceries" },
];

function searchProducts(searchTerm) {
  return new Promise((resolve, reject) => {
    if (!searchTerm || searchTerm.trim() === "") {
      reject(new Error("Search term cannot be empty"));
      return;
    }

    setTimeout(() => {
      const term = searchTerm.toLowerCase();
      const results = products.filter(p =>
        p.name.toLowerCase().includes(term) ||
        p.category.toLowerCase().includes(term)
      );
      resolve(results);
    }, 1000);
  });
}

async function runSearch(query) {
  console.log(`🔍 Searching for: "${query}"...`);
  try {
    const results = await searchProducts(query);

    if (results.length === 0) {
      console.log("No products found.");
    } else {
      console.log(`Found ${results.length} product(s):`);
      results.forEach(p => {
        console.log(`  - ${p.name} — PKR ${p.price}`);
      });
    }
  } catch (error) {
    console.error("Search failed:", error.message);
  }
}

// Test cases
runSearch("rice");
runSearch("Beverages");
runSearch("");       // Should trigger error
runSearch("xyz123"); // Should return "No products found"

Frequently Asked Questions

What is the difference between ES6 and modern JavaScript?

ES6 (also called ECMAScript 2015) was a major update to the JavaScript language that introduced classes, arrow functions, Promises, template literals, let/const, destructuring, and much more. "Modern JavaScript" generally refers to ES6 and everything that came after it (ES7, ES8, ES2019, ES2022, etc.). When developers say they use "modern JavaScript," they almost always mean ES6+ features are included in their code.

What is the difference between a Promise and async/await?

A Promise is an object representing the eventual result of an asynchronous operation. async/await is a syntax built on top of Promises that makes asynchronous code easier to write and read. Under the hood, an async function always returns a Promise. Choosing between them is a matter of style — async/await is generally preferred for readability, while Promises (.then()/.catch()) are useful for certain patterns like Promise.all() or when working in non-async functions.

Can I use JavaScript classes like in Java or Python?

JavaScript classes look and feel similar to classes in Java or Python, but there are important differences. JavaScript uses prototypal inheritance under the hood — the class keyword is syntactic sugar over this system. For practical purposes at the beginner-to-intermediate level, JavaScript classes behave similarly to those in other languages. The main difference to watch for is how this behaves inside class methods, especially when passing methods as callbacks.

How do I handle multiple async operations that depend on each other?

When async operations are sequential (Operation B needs the result of Operation A), chain them using await one after another inside an async function. When operations are independent of each other, use Promise.all() to run them simultaneously and dramatically reduce total wait time. For more complex dependency graphs, you can mix both approaches — run independent groups in parallel, then use their results sequentially.

Is async/await supported in all browsers and Node.js versions?

async/await has excellent support in all modern browsers (Chrome, Firefox, Safari, Edge) and Node.js 7.6+. For most projects in 2024 and beyond, you can safely use async/await without transpilation. If you need to support very old browsers (like Internet Explorer 11), you would need Babel to transpile your code — but IE11 is now well past its end-of-life date and its market share is negligible. For Pakistani government or enterprise projects targeting older systems, check your specific browser requirements.


Summary & Key Takeaways

Here is a quick recap of what you learned in this tutorial:

  • JavaScript Classes provide a clean, readable syntax for object-oriented programming. Use constructor() to initialize properties, extends for inheritance, super() to call parent constructors, and #privateField for encapsulation.
  • Promises represent asynchronous operations with three states — Pending, Fulfilled, and Rejected. Use .then() to handle success, .catch() for errors, and .finally() for cleanup code that always runs.
  • Async/Await makes asynchronous code look synchronous and is built on top of Promises. Every async function returns a Promise, and await pauses execution until a Promise resolves.
  • Error handling is critical in async code. Always wrap await calls in try/catch blocks, and always handle .catch() in Promise chains to prevent unhandled rejections.
  • Performance matters: Use Promise.all() to run independent async operations in parallel instead of awaiting them sequentially — this can dramatically speed up data loading in real applications.
  • Private class fields (#field) introduced in ES2022 provide true encapsulation in JavaScript classes, preventing external code from directly accessing or modifying internal state.

You have taken a big step forward in your JavaScript journey. Here are the recommended tutorials on theiqra.edu.pk to continue building on what you have learned:

Keep coding, stay consistent, and remember: every senior Pakistani developer you admire was once a beginner who kept pushing through the difficult parts. You've got this. 💪


Published on theiqra.edu.pk — Pakistan's Premier Programming Education Platform
Keywords: es6 javascript, javascript classes, javascript promises, async await

Practice the code examples from this tutorial
Open Compiler
Share this tutorial:

Test Your Python Knowledge!

Finished reading? Take a quick quiz to see how much you've learned from this tutorial.

Start Python Quiz

About Zaheer Ahmad