Skip to Content
Module 1: Modern JS & TypeScript1.2 Asynchronous JavaScript

Module 1.2: Asynchronous JavaScript - Promise, Async/Await

Banner illustrating Asynchronous JavaScript concepts

🎯 Learning Objectives

By the end of this module, you will be able to:

  • Grasp the asynchronous (Asynchronous) mechanism in JavaScript.
  • Identify the problem of Callback Hell.
  • Create and manage Promises with their states: Pending, Fulfilled, Rejected.
  • Write readable asynchronous code using Async/Await.
  • Handle errors effectively with Try/Catch.

1. Synchronous vs Asynchronous

1.1. JavaScript Is Single-Threaded

JavaScript operates on a single thread, meaning it can only execute one task at a time.

// Synchronous - Blocking console.log('1. Start'); console.log('2. Processing data'); console.log('3. End'); // Output: 1 -> 2 -> 3 // Asynchronous - Non-blocking console.log('1. Start'); setTimeout(() => { console.log('2. Processing data (after 1 second)'); }, 1000); console.log('3. End'); // Output: 1 -> 3 -> 2

1.2. Why Do We Need Asynchronous Operations?

TaskEstimated TimeIf Synchronous
API Call100ms - 5sUI freezes
Reading Large Files500ms - 10sUnresponsive
Database Query50ms - 2sBlocks the entire app

2. Callbacks

2.1. What Is a Callback Function?

A callback is a function passed into another function and then invoked (called back) after some asynchronous operation completes.

const fetchData = (callback) => { setTimeout(() => { const data = { id: 1, name: 'Alice' }; callback(data); }, 1000); }; fetchData((result) => { console.log('Data:', result); });

2.2. Callback Hell

When multiple asynchronous tasks depend on each other, the code becomes difficult to read:

// ❌ Callback Hell - "Pyramid of Doom" getUser(userId, (err, user) => { if (err) return handleError(err); getOrders(user.id, (err, orders) => { if (err) return handleError(err); getOrderDetails(orders[0].id, (err, details) => { if (err) return handleError(err); console.log('Details:', details); }); }); });

The Problem: Deeply nested code that is hard to read and maintain.

3. Promise

3.1. What Is a Promise?

A Promise is an object that represents the eventual result of an asynchronous operation.

Diagram showing the three states of a Promise: Pending, Fulfilled, and Rejected.

Three States:

StateDescription
PendingWaiting for the result
FulfilledSuccessful, with a resulting value
RejectedFailed, with an error reason

3.2. Creating a Promise

const myPromise = new Promise((resolve, reject) => { const success = true; if (success) { resolve('Success!'); } else { reject(new Error('Failure!')); } });
// Simulating an API call const fetchUser = (userId) => { return new Promise((resolve, reject) => { setTimeout(() => { if (userId > 0) { resolve({ id: userId, name: 'Alice' }); } else { reject(new Error('Invalid user ID')); } }, 1000); }); };

3.3. Handling Promises

fetchUser(1) .then((user) => { console.log('User:', user); return user.id; // Pass the user ID to the next .then }) .then((userId) => { console.log('User ID:', userId); }) .catch((error) => { console.error('Error:', error.message); }) .finally(() => { console.log('Operation complete'); });

3.4. Promise Chaining

// ✅ Solves Callback Hell getUser(userId) .then((user) => getOrders(user.id)) // Chain the next async operation .then((orders) => getOrderDetails(orders[0].id)) .then((details) => { console.log('Details:', details); }) .catch((error) => { console.error('Error:', error.message); // Catch any error from the chain });

3.5. Promise Static Methods

const p1 = fetchUser(1); const p2 = fetchUser(2); const p3 = fetchUser(3); // Promise.all - Waits for ALL promises to fulfill Promise.all([p1, p2, p3]) .then((users) => console.log('All users:', users)) .catch((error) => console.error('One request failed:', error)); // Promise.allSettled - Waits for all promises, regardless of outcome Promise.allSettled([p1, p2, p3]).then((results) => { results.forEach((result) => { if (result.status === 'fulfilled') { console.log('Success:', result.value); } else { console.log('Failed:', result.reason); } }); }); // Promise.race - Returns the result of the first promise to settle Promise.race([p1, p2, p3]).then((firstUser) => console.log('First settled:', firstUser));

4. Async/Await

4.1. Syntax:

Async/Await allows you to write asynchronous code that looks and behaves a bit like synchronous code.

// Declaring an async function async function fetchData() { const result = await somePromise; // 'await' pauses execution until the promise resolves return result; } // Arrow function with async const fetchData = async () => { const result = await somePromise; return result; };

4.2. Promise vs. Async/Await Comparison

// Promise chaining getUser(userId) .then((user) => getOrders(user.id)) .then((orders) => console.log(orders)); // Async/Await - Much more readable! const loadData = async () => { const user = await getUser(userId); const orders = await getOrders(user.id); console.log(orders); };

4.3. Error Handling with Try/Catch

const fetchUserSafely = async (userId) => { try { const user = await fetchUser(userId); const orders = await fetchOrders(user.id); return { success: true, data: { user, orders } }; } catch (error) { console.error('Error:', error.message); return { success: false, error: error.message }; } finally { console.log('Fetch operation finished'); } }; // Using the async function const result = await fetchUserSafely(1); if (result.success) { console.log('Data:', result.data); }

4.4. Parallel vs. Sequential Execution

// ❌ Sequential - Slow const fetchAllSequential = async () => { const user1 = await fetchUser(1); // Waits 1s const user2 = await fetchUser(2); // Waits another 1s const user3 = await fetchUser(3); // Waits another 1s // Total: 3 seconds }; // ✅ Parallel - Fast const fetchAllParallel = async () => { const [user1, user2, user3] = await Promise.all([fetchUser(1), fetchUser(2), fetchUser(3)]); // Total: ~1 second };

5. Real-World Example: Fake API Module

Let’s create a module to simulate API calls with delays:

// fake-api.js // Helper function for introducing delays const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); // Mock database const users = [ { id: 1, name: 'Alice', email: 'alice@example.com' }, { id: 2, name: 'Bob', email: 'bob@example.com' }, { id: 3, name: 'Charlie', email: 'charlie@example.com' }, ]; // GET all users endpoint export const getUsers = async (delayMs = 1000) => { await delay(delayMs); return { success: true, data: [...users] }; // Return a copy to prevent mutations }; // GET user by ID endpoint export const getUserById = async (id, delayMs = 800) => { await delay(delayMs); const user = users.find((u) => u.id === id); if (!user) { return { success: false, error: `User ${id} not found` }; } return { success: true, data: user }; }; // POST create user endpoint export const createUser = async (userData, delayMs = 1000) => { await delay(delayMs); const newUser = { id: users.length + 1, ...userData }; users.push(newUser); return { success: true, data: newUser }; };

Usage Example

// Import functions from our fake API module import { getUsers, getUserById, createUser } from './fake-api.js'; const main = async () => { // Fetch all users with a custom delay const allUsers = await getUsers(500); console.log('All users:', allUsers.data); // Fetch a single user with a custom delay const user = await getUserById(1, 300); console.log('User 1:', user.data); // Making parallel requests using Promise.all const [user1Result, user2Result] = await Promise.all([getUserById(1, 500), getUserById(2, 500)]); console.log('Parallel fetch results:', user1Result.data, user2Result.data); }; // Execute the main async function main();
Last updated on