Skip to main content

Module 18: Async/Await

Async/await is syntactic sugar built on top of Promises, making asynchronous code look and behave like synchronous code. It's the modern standard for async JavaScript.


1. Understanding Async/Await

1.1 What is async/await?

// Promise-based code
function fetchUserPromise(id) {
return fetchUser(id)
.then(user => getOrders(user.id))
.then(orders => getOrderDetails(orders[0].id))
.then(details => console.log(details))
.catch(error => console.error(error));
}

// Async/await (cleaner and more readable)
async function fetchUserAsync(id) {
try {
const user = await fetchUser(id);
const orders = await getOrders(user.id);
const details = await getOrderDetails(orders[0].id);
console.log(details);
} catch (error) {
console.error(error);
}
}
Under the Hood

async/await is syntactic sugar over Promises. An async function always returns a Promise, and await pauses execution until the Promise settles.


2. Async Functions

2.1 Declaring Async Functions

// Function declaration
async function fetchData() {
return 'data';
}

// Function expression
const fetchData = async function() {
return 'data';
};

// Arrow function
const fetchData = async () => {
return 'data';
};

// Method in object
const api = {
async getData() {
return 'data';
}
};

// Method in class
class API {
async getData() {
return 'data';
}
}

2.2 Return Values

// Always returns a Promise
async function getValue() {
return 42;
}

getValue().then(value => console.log(value)); // 42

// Equivalent to:
function getValue() {
return Promise.resolve(42);
}

// Returning a Promise
async function getUser() {
return fetchUser(1); // Returns the Promise
}

getUser().then(user => console.log(user));

3. Await Operator

3.1 Basic Usage

async function fetchUserData() {
// Wait for Promise to resolve
const user = await fetchUser(1);
console.log('User:', user);

// Continue after Promise resolves
const orders = await getOrders(user.id);
console.log('Orders:', orders);

return { user, orders };
}

fetchUserData();

3.2 await Only in Async Functions

// ❌ Error: await outside async function
function regularFunction() {
const data = await fetchData(); // SyntaxError
}

// ✅ Correct
async function asyncFunction() {
const data = await fetchData(); // Works
}

// ✅ IIFE (Immediately Invoked Function Expression)
(async () => {
const data = await fetchData();
console.log(data);
})();

// ✅ Top-level await (ES2022+, in modules)
const data = await fetchData();

3.3 Multiple Await Calls

async function processData() {
// Sequential (waits for each)
const user = await fetchUser(1); // Wait 1 second
const posts = await fetchPosts(1); // Wait 1 second
const comments = await fetchComments(1); // Wait 1 second
// Total: ~3 seconds

return { user, posts, comments };
}

4. Error Handling

4.1 try...catch

async function fetchUser(id) {
try {
const response = await fetch(`https://api.example.com/users/${id}`);

if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}

const user = await response.json();
return user;
} catch (error) {
console.error('Error fetching user:', error.message);
throw error; // Re-throw or handle
}
}

// Usage
fetchUser(1)
.then(user => console.log(user))
.catch(error => console.error('Failed:', error));

4.2 Multiple try...catch Blocks

async function processUser(id) {
let user;

try {
user = await fetchUser(id);
} catch (error) {
console.error('Failed to fetch user:', error);
return null;
}

try {
const orders = await getOrders(user.id);
return { user, orders };
} catch (error) {
console.error('Failed to fetch orders:', error);
return { user, orders: [] };
}
}

4.3 finally Block

async function fetchData() {
let isLoading = true;

try {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
return data;
} catch (error) {
console.error('Error:', error);
throw error;
} finally {
isLoading = false;
console.log('Request completed');
}
}

4.4 Handling Promise Rejections

async function example() {
// Method 1: try...catch
try {
const result = await riskyOperation();
return result;
} catch (error) {
console.error('Error:', error);
}

// Method 2: .catch() on the promise
const result = await riskyOperation().catch(error => {
console.error('Error:', error);
return null; // Fallback value
});

return result;
}

5. Parallel Execution

5.1 Sequential vs Parallel

// ❌ Sequential (slow)
async function fetchSequential() {
const user = await fetchUser(1); // 1 second
const posts = await fetchPosts(1); // 1 second
const comments = await fetchComments(1); // 1 second
// Total: ~3 seconds

return { user, posts, comments };
}

// ✅ Parallel (fast)
async function fetchParallel() {
const [user, posts, comments] = await Promise.all([
fetchUser(1),
fetchPosts(1),
fetchComments(1)
]);
// Total: ~1 second (all run in parallel)

return { user, posts, comments };
}

5.2 Promise.all with async/await

async function fetchAllUsers(ids) {
try {
const users = await Promise.all(
ids.map(id => fetchUser(id))
);
return users;
} catch (error) {
console.error('One of the requests failed:', error);
throw error;
}
}

// Usage
const userIds = [1, 2, 3, 4, 5];
const users = await fetchAllUsers(userIds);
console.log('Users:', users);

5.3 Promise.allSettled

async function fetchAllUsersSettled(ids) {
const results = await Promise.allSettled(
ids.map(id => fetchUser(id))
);

const successful = results
.filter(r => r.status === 'fulfilled')
.map(r => r.value);

const failed = results
.filter(r => r.status === 'rejected')
.map(r => r.reason);

console.log('Successful:', successful.length);
console.log('Failed:', failed.length);

return successful;
}

5.4 Promise.race

async function fetchWithTimeout(url, timeout = 5000) {
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), timeout)
);

try {
const response = await Promise.race([
fetch(url),
timeoutPromise
]);
return await response.json();
} catch (error) {
console.error('Request failed:', error.message);
throw error;
}
}

// Usage
const data = await fetchWithTimeout('https://api.example.com/data', 3000);

6. Common Patterns

6.1 Retry Logic

async function retry(fn, maxAttempts = 3, delay = 1000) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn();
} catch (error) {
if (attempt === maxAttempts) {
throw error;
}
console.log(`Attempt ${attempt} failed, retrying...`);
await sleep(delay);
}
}
}

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

// Usage
const data = await retry(() => fetchData(), 3, 1000);

6.2 Sequential Processing

async function processItemsSequentially(items) {
const results = [];

for (const item of items) {
const result = await processItem(item);
results.push(result);
}

return results;
}

// Or with reduce
async function processItemsSequentially(items) {
return await items.reduce(async (promiseChain, item) => {
const results = await promiseChain;
const result = await processItem(item);
return [...results, result];
}, Promise.resolve([]));
}

6.3 Batch Processing

async function processBatch(items, batchSize = 5) {
const results = [];

for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
const batchResults = await Promise.all(
batch.map(item => processItem(item))
);
results.push(...batchResults);
}

return results;
}

6.4 Rate Limiting

class RateLimiter {
constructor(maxConcurrent, minDelay = 0) {
this.maxConcurrent = maxConcurrent;
this.minDelay = minDelay;
this.running = 0;
this.queue = [];
}

async execute(fn) {
while (this.running >= this.maxConcurrent) {
await new Promise(resolve => this.queue.push(resolve));
}

this.running++;

try {
return await fn();
} finally {
this.running--;

if (this.minDelay > 0) {
await sleep(this.minDelay);
}

const resolve = this.queue.shift();
if (resolve) resolve();
}
}
}

// Usage
const limiter = new RateLimiter(3, 100); // Max 3 concurrent, 100ms between

const promises = urls.map(url =>
limiter.execute(() => fetch(url))
);

const responses = await Promise.all(promises);

7. Practical Examples

7.1 API Client

class APIClient {
constructor(baseURL) {
this.baseURL = baseURL;
this.token = null;
}

async setToken(token) {
this.token = token;
}

async request(endpoint, options = {}) {
const url = `${this.baseURL}${endpoint}`;
const headers = {
'Content-Type': 'application/json',
...options.headers
};

if (this.token) {
headers['Authorization'] = `Bearer ${this.token}`;
}

try {
const response = await fetch(url, {
...options,
headers
});

if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}

return await response.json();
} catch (error) {
console.error('API request failed:', error);
throw error;
}
}

async get(endpoint) {
return await this.request(endpoint, { method: 'GET' });
}

async post(endpoint, data) {
return await this.request(endpoint, {
method: 'POST',
body: JSON.stringify(data)
});
}

async put(endpoint, data) {
return await this.request(endpoint, {
method: 'PUT',
body: JSON.stringify(data)
});
}

async delete(endpoint) {
return await this.request(endpoint, { method: 'DELETE' });
}
}

// Usage
const api = new APIClient('https://api.example.com');

async function main() {
try {
const users = await api.get('/users');
console.log('Users:', users);

const newUser = await api.post('/users', {
name: 'John Doe',
email: 'john@example.com'
});
console.log('Created user:', newUser);
} catch (error) {
console.error('Error:', error);
}
}

7.2 Data Fetching with Caching

class DataCache {
constructor(ttl = 60000) {
this.cache = new Map();
this.ttl = ttl;
}

async get(key, fetcher) {
const cached = this.cache.get(key);

if (cached && Date.now() - cached.timestamp < this.ttl) {
return cached.data;
}

const data = await fetcher();
this.cache.set(key, {
data,
timestamp: Date.now()
});

return data;
}

clear() {
this.cache.clear();
}
}

// Usage
const cache = new DataCache(60000); // 1 minute TTL

async function getUser(id) {
return await cache.get(`user:${id}`, async () => {
const response = await fetch(`https://api.example.com/users/${id}`);
return await response.json();
});
}

// First call: fetches from API
const user1 = await getUser(1);

// Second call within 1 minute: returns cached data
const user2 = await getUser(1);

7.3 Parallel Data Loading

async function loadUserDashboard(userId) {
try {
// Start all requests in parallel
const [user, posts, followers, notifications] = await Promise.all([
fetchUser(userId),
fetchUserPosts(userId),
fetchFollowers(userId),
fetchNotifications(userId)
]);

// Process data
const dashboard = {
user,
stats: {
posts: posts.length,
followers: followers.length,
unreadNotifications: notifications.filter(n => !n.read).length
},
recentPosts: posts.slice(0, 5),
recentFollowers: followers.slice(0, 10)
};

return dashboard;
} catch (error) {
console.error('Failed to load dashboard:', error);
throw error;
}
}

// Usage
const dashboard = await loadUserDashboard(123);
console.log('Dashboard:', dashboard);

7.4 Form Submission with Validation

class FormHandler {
constructor(formId) {
this.form = document.getElementById(formId);
this.errors = {};
}

async validate(data) {
this.errors = {};

if (!data.email || !data.email.includes('@')) {
this.errors.email = 'Invalid email address';
}

if (!data.password || data.password.length < 8) {
this.errors.password = 'Password must be at least 8 characters';
}

// Async validation: check if email exists
if (data.email && !this.errors.email) {
try {
const response = await fetch(`/api/check-email?email=${data.email}`);
const { exists } = await response.json();

if (exists) {
this.errors.email = 'Email already registered';
}
} catch (error) {
console.error('Validation error:', error);
}
}

return Object.keys(this.errors).length === 0;
}

async submit(data) {
try {
const isValid = await this.validate(data);

if (!isValid) {
this.showErrors();
return null;
}

const response = await fetch('/api/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});

if (!response.ok) {
throw new Error('Registration failed');
}

return await response.json();
} catch (error) {
console.error('Submission error:', error);
throw error;
}
}

showErrors() {
for (const [field, message] of Object.entries(this.errors)) {
console.error(`${field}: ${message}`);
}
}
}

8. Best Practices

8.1 Always Handle Errors

// ❌ Bad: Unhandled promise rejection
async function fetchData() {
const data = await fetch('/api/data');
return data.json();
}

// ✅ Good: Handle errors
async function fetchData() {
try {
const response = await fetch('/api/data');
if (!response.ok) throw new Error('Fetch failed');
return await response.json();
} catch (error) {
console.error('Error:', error);
throw error;
}
}

8.2 Avoid Sequential When Parallel is Possible

// ❌ Bad: Unnecessary sequential execution
async function fetchData() {
const users = await fetchUsers();
const posts = await fetchPosts();
const comments = await fetchComments();
return { users, posts, comments };
}

// ✅ Good: Parallel execution
async function fetchData() {
const [users, posts, comments] = await Promise.all([
fetchUsers(),
fetchPosts(),
fetchComments()
]);
return { users, posts, comments };
}

8.3 Use async/await with Array Methods

// ❌ Bad: forEach doesn't wait
async function processItems(items) {
items.forEach(async (item) => {
await processItem(item); // Doesn't wait!
});
console.log('Done'); // Logs before items are processed
}

// ✅ Good: Use for...of for sequential
async function processItems(items) {
for (const item of items) {
await processItem(item);
}
console.log('Done'); // Logs after all items processed
}

// ✅ Good: Use Promise.all for parallel
async function processItems(items) {
await Promise.all(items.map(item => processItem(item)));
console.log('Done');
}

8.4 Top-Level Await (ES2022+)

// In module scope (ES modules)
const data = await fetchData();
const user = await getUser(1);

console.log('Data:', data);
console.log('User:', user);

// Or wrap in IIFE for older environments
(async () => {
const data = await fetchData();
console.log('Data:', data);
})();

9. Common Mistakes

9.1 Forgetting await

// ❌ Bad
async function getData() {
const data = fetch('/api/data'); // Missing await!
console.log(data); // Promise, not the data
return data;
}

// ✅ Good
async function getData() {
const data = await fetch('/api/data');
console.log(data);
return data;
}

9.2 Using await in Loops Unnecessarily

// ❌ Bad: Sequential (slow)
for (let i = 0; i < ids.length; i++) {
const user = await fetchUser(ids[i]);
users.push(user);
}

// ✅ Good: Parallel (fast)
const users = await Promise.all(
ids.map(id => fetchUser(id))
);

9.3 Not Returning from Async Functions

// ❌ Bad
async function processData() {
const data = await fetchData();
processResult(data); // Forgotten await
}

// ✅ Good
async function processData() {
const data = await fetchData();
return await processResult(data);
}

Summary

In this module, you learned:

  • ✅ What async/await is and how it works
  • ✅ Declaring and using async functions
  • ✅ Error handling with try...catch
  • ✅ Parallel execution with Promise.all
  • ✅ Common patterns: retry, batching, rate limiting
  • ✅ Practical examples: API clients, caching, form handling
  • ✅ Best practices and common mistakes
  • ✅ Modern features like top-level await
Next Steps

In Module 19, you'll learn about Modules (Import/Export) for organizing code into reusable components.


Practice Exercises

  1. Convert Promise-based code to async/await
  2. Build an async API client with retry logic
  3. Implement parallel data fetching with error handling
  4. Create a batch processor for large datasets
  5. Build a rate-limited web scraper
  6. Implement an async cache with expiration
  7. Create a form with async validation
  8. Build a concurrent task queue

Additional Resources