JavaScript Async/Await: Complete Guide to Asynchronous Programming
Master async/await in JavaScript. Learn promises, async functions, error handling, parallel execution, and best practices for asynchronous code.
JavaScript Async/Await: Complete Guide to Asynchronous Programming
Async/await is a modern way to handle asynchronous operations in JavaScript. Built on top of Promises, it makes asynchronous code look and behave like synchronous code, making it easier to read and maintain.
Understanding Asynchronous JavaScript
Before diving into async/await, let's understand why we need asynchronous programming:
// Synchronous code - blocks execution
console.log('Start');
const data = fetchDataSync(); // This would block
console.log(data);
console.log('End');
// Asynchronous code - non-blocking
console.log('Start');
fetchDataAsync().then((data) => {
console.log(data);
});
console.log('End'); // This runs before data is fetched
Promises: The Foundation
Async/await is built on Promises, so understanding them is crucial:
// Creating a Promise
const myPromise = new Promise((resolve, reject) => {
setTimeout(() => {
const success = true;
if (success) {
resolve('Operation successful!');
} else {
reject('Operation failed!');
}
}, 1000);
});
// Using a Promise
myPromise
.then((result) => console.log(result))
.catch((error) => console.error(error));
// Promise states:
// 1. Pending - initial state
// 2. Fulfilled - operation completed successfully
// 3. Rejected - operation failed
Introduction to Async/Await
Async/await provides a cleaner syntax for working with Promises:
// Promise approach
function fetchUser() {
return fetch('/api/user')
.then((response) => response.json())
.then((data) => {
console.log(data);
return data;
})
.catch((error) => {
console.error('Error:', error);
});
}
// Async/await approach
async function fetchUser() {
try {
const response = await fetch('/api/user');
const data = await response.json();
console.log(data);
return data;
} catch (error) {
console.error('Error:', error);
}
}
Async Functions
An async function always returns a Promise:
// Basic async function
async function greet() {
return 'Hello, World!';
}
// Equivalent to:
function greet() {
return Promise.resolve('Hello, World!');
}
// Using the function
greet().then((message) => console.log(message)); // 'Hello, World!'
// Async arrow functions
const greetArrow = async () => 'Hello, World!';
// Async function expressions
const greetExpr = async function () {
return 'Hello, World!';
};
// Async methods in objects
const obj = {
async method() {
return 'Hello from method!';
},
};
// Async methods in classes
class Greeter {
async greet() {
return 'Hello from class!';
}
}
The Await Keyword
await
pauses the execution of an async function until the Promise is resolved:
async function fetchData() {
console.log('Fetching data...');
// await pauses here until fetch completes
const response = await fetch('/api/data');
console.log('Got response');
// await pauses again for json parsing
const data = await response.json();
console.log('Parsed data:', data);
return data;
}
// await can only be used inside async functions
// This would cause an error:
// const data = await fetch('/api/data'); // SyntaxError
// But top-level await is supported in modules:
// In a .mjs file or <script type="module">
const response = await fetch('/api/data');
Error Handling
Try-Catch Blocks
async function fetchUserData(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error('Failed to fetch user:', error);
// Re-throw to let caller handle it
throw error;
}
}
// Using the function
async function displayUser(userId) {
try {
const user = await fetchUserData(userId);
console.log('User:', user);
} catch (error) {
console.log('Could not display user');
}
}
Error Handling Patterns
// Pattern 1: Individual try-catch
async function pattern1() {
let user, posts;
try {
user = await fetchUser();
} catch (error) {
console.error('User fetch failed:', error);
user = null;
}
try {
posts = await fetchPosts();
} catch (error) {
console.error('Posts fetch failed:', error);
posts = [];
}
return { user, posts };
}
// Pattern 2: Catch method
async function pattern2() {
const user = await fetchUser().catch((err) => {
console.error('User fetch failed:', err);
return null;
});
const posts = await fetchPosts().catch((err) => {
console.error('Posts fetch failed:', err);
return [];
});
return { user, posts };
}
// Pattern 3: Error wrapper
async function safeAwait(promise) {
try {
const data = await promise;
return [data, null];
} catch (error) {
return [null, error];
}
}
async function pattern3() {
const [user, userError] = await safeAwait(fetchUser());
const [posts, postsError] = await safeAwait(fetchPosts());
if (userError) console.error('User error:', userError);
if (postsError) console.error('Posts error:', postsError);
return { user, posts };
}
Sequential vs Parallel Execution
Sequential Execution
// Each await waits for the previous one
async function sequential() {
console.time('sequential');
const user = await fetchUser(); // 1 second
const posts = await fetchPosts(); // 1 second
const comments = await fetchComments(); // 1 second
console.timeEnd('sequential'); // ~3 seconds
return { user, posts, comments };
}
Parallel Execution
// Method 1: Promise.all - fails if any promise rejects
async function parallel1() {
console.time('parallel1');
const [user, posts, comments] = await Promise.all([
fetchUser(),
fetchPosts(),
fetchComments(),
]);
console.timeEnd('parallel1'); // ~1 second
return { user, posts, comments };
}
// Method 2: Start all, await individually
async function parallel2() {
console.time('parallel2');
// Start all requests
const userPromise = fetchUser();
const postsPromise = fetchPosts();
const commentsPromise = fetchComments();
// Await results
const user = await userPromise;
const posts = await postsPromise;
const comments = await commentsPromise;
console.timeEnd('parallel2'); // ~1 second
return { user, posts, comments };
}
// Method 3: Promise.allSettled - doesn't fail if some reject
async function parallel3() {
const results = await Promise.allSettled([
fetchUser(),
fetchPosts(),
fetchComments(),
]);
const user = results[0].status === 'fulfilled' ? results[0].value : null;
const posts = results[1].status === 'fulfilled' ? results[1].value : [];
const comments = results[2].status === 'fulfilled' ? results[2].value : [];
return { user, posts, comments };
}
Advanced Patterns
Async Iteration
// Async generator function
async function* asyncGenerator() {
yield await Promise.resolve(1);
yield await Promise.resolve(2);
yield await Promise.resolve(3);
}
// Using async iteration
async function useAsyncIterator() {
for await (const value of asyncGenerator()) {
console.log(value); // 1, 2, 3
}
}
// Real-world example: paginated API
async function* fetchPages(url) {
let nextUrl = url;
while (nextUrl) {
const response = await fetch(nextUrl);
const data = await response.json();
yield data.items;
nextUrl = data.nextPage;
}
}
// Using paginated data
async function getAllItems() {
const allItems = [];
for await (const items of fetchPages('/api/items')) {
allItems.push(...items);
}
return allItems;
}
Rate Limiting and Queuing
class RateLimiter {
constructor(maxConcurrent = 5) {
this.maxConcurrent = maxConcurrent;
this.running = 0;
this.queue = [];
}
async run(fn) {
while (this.running >= this.maxConcurrent) {
await new Promise((resolve) => this.queue.push(resolve));
}
this.running++;
try {
return await fn();
} finally {
this.running--;
const next = this.queue.shift();
if (next) next();
}
}
}
// Usage
const limiter = new RateLimiter(3);
async function fetchWithLimit(urls) {
const promises = urls.map((url) =>
limiter.run(() => fetch(url).then((r) => r.json()))
);
return Promise.all(promises);
}
Retry Logic
async function retry(fn, retries = 3, delay = 1000) {
try {
return await fn();
} catch (error) {
if (retries === 0) throw error;
console.log(`Retrying... ${retries} attempts left`);
await new Promise((resolve) => setTimeout(resolve, delay));
return retry(fn, retries - 1, delay * 2); // Exponential backoff
}
}
// Usage
async function fetchWithRetry(url) {
return retry(
() =>
fetch(url).then((r) => {
if (!r.ok) throw new Error(`HTTP ${r.status}`);
return r.json();
}),
3,
1000
);
}
Timeout Implementation
function timeout(ms) {
return new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), ms)
);
}
async function fetchWithTimeout(url, ms = 5000) {
try {
const response = await Promise.race([fetch(url), timeout(ms)]);
return await response.json();
} catch (error) {
if (error.message === 'Timeout') {
console.error('Request timed out');
}
throw error;
}
}
// Alternative using AbortController
async function fetchWithAbort(url, ms = 5000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), ms);
try {
const response = await fetch(url, { signal: controller.signal });
clearTimeout(timeoutId);
return await response.json();
} catch (error) {
if (error.name === 'AbortError') {
console.error('Request aborted');
}
throw error;
}
}
Common Use Cases
API Calls
class ApiClient {
constructor(baseUrl) {
this.baseUrl = baseUrl;
}
async request(endpoint, options = {}) {
const url = `${this.baseUrl}${endpoint}`;
try {
const response = await fetch(url, {
headers: {
'Content-Type': 'application/json',
...options.headers,
},
...options,
});
if (!response.ok) {
throw new Error(`API Error: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('API Request failed:', error);
throw error;
}
}
async get(endpoint) {
return this.request(endpoint, { method: 'GET' });
}
async post(endpoint, data) {
return this.request(endpoint, {
method: 'POST',
body: JSON.stringify(data),
});
}
}
// Usage
const api = new ApiClient('https://api.example.com');
async function createUser(userData) {
try {
const user = await api.post('/users', userData);
console.log('User created:', user);
return user;
} catch (error) {
console.error('Failed to create user:', error);
}
}
File Operations
// Reading files in Node.js
import { promises as fs } from 'fs';
async function readConfig() {
try {
const data = await fs.readFile('config.json', 'utf8');
return JSON.parse(data);
} catch (error) {
console.error('Failed to read config:', error);
return {};
}
}
// Processing multiple files
async function processFiles(filePaths) {
const results = await Promise.all(
filePaths.map(async (path) => {
const content = await fs.readFile(path, 'utf8');
// Process content
return processContent(content);
})
);
return results;
}
Database Operations
class Database {
async connect() {
// Simulate connection
await new Promise((resolve) => setTimeout(resolve, 100));
console.log('Connected to database');
}
async query(sql, params) {
// Simulate query
await new Promise((resolve) => setTimeout(resolve, 50));
return { rows: [] };
}
async transaction(callback) {
await this.query('BEGIN');
try {
const result = await callback(this);
await this.query('COMMIT');
return result;
} catch (error) {
await this.query('ROLLBACK');
throw error;
}
}
}
// Usage
async function transferMoney(db, fromId, toId, amount) {
return db.transaction(async (tx) => {
const from = await tx.query('SELECT balance FROM accounts WHERE id = ?', [
fromId,
]);
if (from.rows[0].balance < amount) {
throw new Error('Insufficient funds');
}
await tx.query('UPDATE accounts SET balance = balance - ? WHERE id = ?', [
amount,
fromId,
]);
await tx.query('UPDATE accounts SET balance = balance + ? WHERE id = ?', [
amount,
toId,
]);
return { success: true, amount };
});
}
Performance Considerations
// Bad: Sequential when parallel is possible
async function slowVersion(ids) {
const results = [];
for (const id of ids) {
const data = await fetchData(id); // Each waits for previous
results.push(data);
}
return results;
}
// Good: Parallel execution
async function fastVersion(ids) {
const promises = ids.map((id) => fetchData(id));
return Promise.all(promises);
}
// Bad: Creating unnecessary promises
async function unnecessary() {
return await Promise.resolve(42); // await is unnecessary
}
// Good: Direct return
async function better() {
return 42; // Automatically wrapped in Promise
}
// Memory consideration for large datasets
async function* processBatches(items, batchSize = 100) {
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
const results = await Promise.all(batch.map((item) => processItem(item)));
yield results;
}
}
Common Pitfalls and Solutions
Forgetting await
// Pitfall: Missing await
async function bug() {
const data = fetch('/api/data'); // Returns Promise, not data!
console.log(data.name); // Error: Cannot read property 'name' of Promise
}
// Solution
async function fixed() {
const response = await fetch('/api/data');
const data = await response.json();
console.log(data.name);
}
Array methods with async
// Pitfall: forEach doesn't wait
async function wrong() {
const ids = [1, 2, 3];
ids.forEach(async (id) => {
await processId(id); // These run in parallel, not sequentially
});
console.log('Done'); // Logs before processing finishes
}
// Solution 1: for...of loop
async function correct1() {
const ids = [1, 2, 3];
for (const id of ids) {
await processId(id); // Sequential
}
console.log('Done');
}
// Solution 2: Promise.all with map
async function correct2() {
const ids = [1, 2, 3];
await Promise.all(ids.map((id) => processId(id))); // Parallel
console.log('Done');
}
Error swallowing
// Pitfall: Errors not propagated
async function swallowError() {
try {
await riskyOperation();
} catch (error) {
console.log(error); // Only logs, doesn't propagate
}
}
// Solution: Re-throw when needed
async function propagateError() {
try {
await riskyOperation();
} catch (error) {
console.error('Operation failed:', error);
throw error; // Re-throw for caller to handle
}
}
Best Practices
- Always handle errors - Use try-catch or .catch()
- Prefer async/await over Promise chains for readability
- Use Promise.all() for parallel operations
- Don't await unnecessarily - Return promises directly when possible
- Be careful with loops - Understand sequential vs parallel execution
- Add timeouts for external requests
- Use proper error messages - Include context in errors
- Test async code properly - Use async test frameworks
Conclusion
Async/await has revolutionized asynchronous programming in JavaScript by making it more readable and maintainable. Key takeaways:
- Async functions always return Promises
await
pauses execution until Promise resolves- Proper error handling with try-catch is crucial
- Understanding sequential vs parallel execution improves performance
- Modern patterns like retry logic and rate limiting enhance robustness
Master async/await to write cleaner, more efficient asynchronous JavaScript code!