Async/await is a modern way to handle asynchronous operations in JavaScript. It makes asynchronous code look and behave more like synchronous code, making it easier to read and maintain.
Understanding Asynchronous JavaScript #
JavaScript is single-threaded, meaning it can only execute one operation at a time. Asynchronous operations allow JavaScript to perform tasks like API calls, file operations, or timers without blocking the main thread.
The Evolution of Async JavaScript #
Callbacks (The Old Way) #
function getData(callback) {
setTimeout(() => {
callback('Data received');
}, 1000);
}
getData((data) => {
console.log(data);
});Problem: Callback hell - nested callbacks become difficult to read and maintain.
Promises (Better) #
function getData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Data received');
}, 1000);
});
}
getData()
.then(data => console.log(data))
.catch(error => console.error(error));Async/Await (Best) #
async function getData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Data received');
}, 1000);
});
}
async function main() {
const data = await getData();
console.log(data);
}
main();Basic Async/Await Syntax #
The async Keyword #
Adding async before a function makes it return a Promise automatically:
async function sayHello() {
return 'Hello!';
}
// This is equivalent to:
function sayHello() {
return Promise.resolve('Hello!');
}
// Usage:
sayHello().then(message => console.log(message)); // "Hello!"
The await Keyword #
await pauses the execution of an async function until the Promise resolves:
async function fetchUser() {
const response = await fetch('https://api.example.com/user/1');
const user = await response.json();
return user;
}Important: You can only use await inside an async function.
Practical Examples #
Making API Calls #
async function fetchUserData(userId) {
try {
const response = await fetch(`https://api.example.com/users/${userId}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const user = await response.json();
return user;
} catch (error) {
console.error('Error fetching user:', error);
throw error;
}
}
// Usage
async function main() {
const user = await fetchUserData(123);
console.log(user);
}
main();Sequential vs Parallel Execution #
Sequential (Slower):
async function getDataSequential() {
const user = await fetch('https://api.example.com/user/1').then(r => r.json());
const posts = await fetch('https://api.example.com/posts/1').then(r => r.json());
const comments = await fetch('https://api.example.com/comments/1').then(r => r.json());
return { user, posts, comments };
}
// Total time: Time1 + Time2 + Time3
Parallel (Faster):
async function getDataParallel() {
const [user, posts, comments] = await Promise.all([
fetch('https://api.example.com/user/1').then(r => r.json()),
fetch('https://api.example.com/posts/1').then(r => r.json()),
fetch('https://api.example.com/comments/1').then(r => r.json())
]);
return { user, posts, comments };
}
// Total time: Max(Time1, Time2, Time3)
Error Handling with Try/Catch #
async function fetchWithErrorHandling() {
try {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
return data;
} catch (error) {
console.error('Failed to fetch data:', error);
return null;
}
}Multiple Try/Catch Blocks #
async function complexOperation() {
let user;
let posts;
try {
user = await fetchUser();
} catch (error) {
console.error('Failed to fetch user:', error);
user = { name: 'Guest' }; // Fallback
}
try {
posts = await fetchPosts(user.id);
} catch (error) {
console.error('Failed to fetch posts:', error);
posts = []; // Fallback
}
return { user, posts };
}Advanced Patterns #
Async IIFE (Immediately Invoked Function Expression) #
(async () => {
const data = await fetchData();
console.log(data);
})();Async Array Methods #
Using for…of (Correct):
async function processUsers(userIds) {
const results = [];
for (const id of userIds) {
const user = await fetchUser(id);
results.push(user);
}
return results;
}Using map with Promise.all (Parallel):
async function processUsersParallel(userIds) {
const promises = userIds.map(id => fetchUser(id));
const results = await Promise.all(promises);
return results;
}Warning: Regular forEach won’t work with await:
// This DOESN'T work as expected
userIds.forEach(async (id) => {
const user = await fetchUser(id); // Runs immediately, doesn't wait
console.log(user);
});Promise.all vs Promise.allSettled #
Promise.all - Fails if any promise rejects:
async function fetchAll() {
try {
const [user, posts, comments] = await Promise.all([
fetchUser(),
fetchPosts(),
fetchComments()
]);
return { user, posts, comments };
} catch (error) {
// If any request fails, we end up here
console.error('One of the requests failed:', error);
}
}Promise.allSettled - Waits for all promises, regardless of success/failure:
async function fetchAllSettled() {
const results = await Promise.allSettled([
fetchUser(),
fetchPosts(),
fetchComments()
]);
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`Request ${index} succeeded:`, result.value);
} else {
console.log(`Request ${index} failed:`, result.reason);
}
});
return results;
}Async Functions with Timeout #
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;
}
}Retry Logic #
async function fetchWithRetry(url, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
const response = await fetch(url);
return await response.json();
} catch (error) {
console.log(`Attempt ${i + 1} failed`);
if (i === maxRetries - 1) {
throw error; // Final attempt failed
}
// Wait before retrying (exponential backoff)
await new Promise(resolve => setTimeout(resolve, 1000 * Math.pow(2, i)));
}
}
}Common Mistakes and How to Avoid Them #
Mistake 1: Forgetting await #
// Wrong - Returns a Promise, not the data
async function getData() {
const data = fetch('https://api.example.com/data');
return data; // Promise<Response>
}
// Correct
async function getData() {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
return data;
}Mistake 2: Not Handling Errors #
// Risky - Errors will crash the application
async function fetchData() {
const data = await fetch('https://api.example.com/data');
return data;
}
// Safe
async function fetchData() {
try {
const response = await fetch('https://api.example.com/data');
return await response.json();
} catch (error) {
console.error('Fetch failed:', error);
return null;
}
}Mistake 3: Sequential When You Want Parallel #
// Slow - Takes 2 seconds
async function slow() {
const a = await delay(1000);
const b = await delay(1000);
return [a, b];
}
// Fast - Takes 1 second
async function fast() {
const [a, b] = await Promise.all([
delay(1000),
delay(1000)
]);
return [a, b];
}Best Practices #
- Always handle errors with try/catch or .catch()
- Use async/await over .then() for cleaner code
- Run independent operations in parallel with Promise.all
- Don’t use await in loops unless you need sequential execution
- Return early from async functions when possible
- Add timeouts for network requests
- Use async IIFE for top-level await (before ES2022)
Browser and Node.js Support #
Async/await is supported in:
- Node.js 7.6+
- Chrome 55+
- Firefox 52+
- Safari 10.1+
- Edge 15+
For older browsers, use transpilers like Babel.
Async/await makes asynchronous JavaScript significantly more readable and maintainable. Master these patterns to write better JavaScript code.