Module 1.2: Asynchronous JavaScript - Promise, Async/Await

🎯 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 -> 21.2. Why Do We Need Asynchronous Operations?
| Task | Estimated Time | If Synchronous |
|---|---|---|
| API Call | 100ms - 5s | UI freezes |
| Reading Large Files | 500ms - 10s | Unresponsive |
| Database Query | 50ms - 2s | Blocks 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.

Three States:
| State | Description |
|---|---|
| Pending | Waiting for the result |
| Fulfilled | Successful, with a resulting value |
| Rejected | Failed, 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