Asynchronous JavaScript - Promises, Async/Await, and Fetch
Master asynchronous programming in JavaScript with promises, async/await, and the Fetch API. Learn to handle API calls, timers, and concurrent operations.
Asynchronous JavaScript: Promises, Async/Await, and Fetch
JavaScript is single-threaded, but it can handle asynchronous operations efficiently using promises, async/await, and various APIs. This guide covers everything you need to know about asynchronous programming in JavaScript.
Understanding Asynchronous Programming #
Synchronous code executes line by line, blocking the next line until the current one completes. Asynchronous code allows other operations to continue while waiting for long-running tasks to complete.
// Synchronous (blocking)
console.log('Start');
console.log('Middle');
console.log('End');
// Output: Start, Middle, End (in order)
// Asynchronous (non-blocking)
console.log('Start');
setTimeout(() => {
console.log('Middle');
}, 0);
console.log('End');
// Output: Start, End, Middle
The Problem with Callbacks #
Before promises, JavaScript used callbacks for asynchronous operations, which led to "callback hell":
// Callback hell - hard to read and maintain
getData(function(a) {
getMoreData(a, function(b) {
getEvenMoreData(b, function(c) {
getFinalData(c, function(d) {
// Finally got the data
console.log(d);
});
});
});
});
Introduction to Promises #
Promises provide a cleaner way to handle asynchronous operations:
// Promise syntax
const myPromise = new Promise((resolve, reject) => {
// Asynchronous operation
const success = true;
if (success) {
resolve('Operation successful!');
} else {
reject('Operation failed!');
}
});
// Using the promise
myPromise
.then(result => {
console.log(result); // 'Operation successful!'
})
.catch(error => {
console.log(error); // 'Operation failed!'
});
Promise States #
A promise has three states:
- Pending: Initial state, neither fulfilled nor rejected
- Fulfilled: Operation completed successfully
- Rejected: Operation failed
const pendingPromise = new Promise((resolve, reject) => {
// Promise is pending
});
const fulfilledPromise = Promise.resolve('Success!');
const rejectedPromise = Promise.reject('Error!');
Working with Promises #
Promise.then() and Promise.catch() #
function fetchUserData(userId) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (userId > 0) {
resolve({ id: userId, name: 'John Doe', email: '[email protected]' });
} else {
reject('Invalid user ID');
}
}, 1000);
});
}
// Using the promise
fetchUserData(1)
.then(user => {
console.log('User data:', user);
return user.email; // Return value for next .then()
})
.then(email => {
console.log('User email:', email);
})
.catch(error => {
console.error('Error:', error);
})
.finally(() => {
console.log('Operation completed');
});
Promise Chaining #
function getUser(id) {
return new Promise(resolve => {
setTimeout(() => resolve({ id, name: 'Alice' }), 1000);
});
}
function getPosts(userId) {
return new Promise(resolve => {
setTimeout(() => resolve(['Post 1', 'Post 2']), 1000);
});
}
function getComments(postId) {
return new Promise(resolve => {
setTimeout(() => resolve(['Comment 1', 'Comment 2']), 1000);
});
}
// Chaining promises
getUser(1)
.then(user => {
console.log('User:', user);
return getPosts(user.id);
})
.then(posts => {
console.log('Posts:', posts);
return getComments(posts[0]);
})
.then(comments => {
console.log('Comments:', comments);
})
.catch(error => {
console.error('Error:', error);
});
Promise.all() and Promise.allSettled() #
const promise1 = Promise.resolve(3);
const promise2 = new Promise(resolve => setTimeout(() => resolve('foo'), 1000));
const promise3 = Promise.resolve(42);
// Wait for all promises to resolve
Promise.all([promise1, promise2, promise3])
.then(values => {
console.log(values); // [3, 'foo', 42]
})
.catch(error => {
console.error('One or more promises failed:', error);
});
// Wait for all promises to settle (resolve or reject)
Promise.allSettled([promise1, promise2, promise3])
.then(results => {
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`Promise ${index + 1} resolved:`, result.value);
} else {
console.log(`Promise ${index + 1} rejected:`, result.reason);
}
});
});
Promise.race() #
const slowPromise = new Promise(resolve => setTimeout(() => resolve('slow'), 2000));
const fastPromise = new Promise(resolve => setTimeout(() => resolve('fast'), 1000));
Promise.race([slowPromise, fastPromise])
.then(result => {
console.log('First to complete:', result); // 'fast'
});
Async/Await - Modern Asynchronous JavaScript #
Async/await provides a cleaner, more readable way to work with promises:
// Using promises
function fetchDataWithPromises() {
return getUser(1)
.then(user => {
console.log('User:', user);
return getPosts(user.id);
})
.then(posts => {
console.log('Posts:', posts);
return posts;
})
.catch(error => {
console.error('Error:', error);
});
}
// Using async/await
async function fetchDataWithAsyncAwait() {
try {
const user = await getUser(1);
console.log('User:', user);
const posts = await getPosts(user.id);
console.log('Posts:', posts);
return posts;
} catch (error) {
console.error('Error:', error);
}
}
Async Function Syntax #
// Async function declaration
async function myAsyncFunction() {
return 'Hello World';
}
// Async function expression
const myAsyncFunction = async function() {
return 'Hello World';
};
// Async arrow function
const myAsyncArrowFunction = async () => {
return 'Hello World';
};
// All return a Promise
myAsyncFunction().then(result => console.log(result)); // 'Hello World'
Error Handling with Async/Await #
async function fetchUserWithErrorHandling(userId) {
try {
const user = await fetchUserData(userId);
const posts = await getPosts(user.id);
const comments = await getComments(posts[0]);
return { user, posts, comments };
} catch (error) {
console.error('Something went wrong:', error);
throw error; // Re-throw if needed
} finally {
console.log('Cleanup operations');
}
}
// Using the async function
fetchUserWithErrorHandling(1)
.then(data => console.log('All data:', data))
.catch(error => console.error('Final error handler:', error));
The Fetch API #
The Fetch API provides a modern way to make HTTP requests:
// Basic fetch
fetch('https://jsonplaceholder.typicode.com/posts/1')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
// Fetch with async/await
async function fetchPost(id) {
try {
const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error('Fetch error:', error);
throw error;
}
}
// Using the fetch function
fetchPost(1)
.then(post => console.log('Post:', post))
.catch(error => console.error('Error fetching post:', error));
Fetch Options #
// POST request with JSON data
async function createPost(postData) {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/posts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer your-token-here'
},
body: JSON.stringify(postData)
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
return result;
} catch (error) {
console.error('Error creating post:', error);
throw error;
}
}
// Usage
createPost({
title: 'My New Post',
body: 'This is the content of my post',
userId: 1
})
.then(result => console.log('Created post:', result))
.catch(error => console.error('Error:', error));
Handling Different Response Types #
async function fetchWithDifferentTypes(url) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('application/json')) {
return await response.json();
} else if (contentType && contentType.includes('text/')) {
return await response.text();
} else {
return await response.blob();
}
} catch (error) {
console.error('Fetch error:', error);
throw error;
}
}
Concurrent Operations #
Running Multiple Async Operations #
// Sequential execution (slower)
async function sequentialExecution() {
const start = Date.now();
const user = await fetchUser(1);
const posts = await fetchPosts(1);
const comments = await fetchComments(1);
const end = Date.now();
console.log(`Sequential took ${end - start}ms`);
return { user, posts, comments };
}
// Concurrent execution (faster)
async function concurrentExecution() {
const start = Date.now();
const [user, posts, comments] = await Promise.all([
fetchUser(1),
fetchPosts(1),
fetchComments(1)
]);
const end = Date.now();
console.log(`Concurrent took ${end - start}ms`);
return { user, posts, comments };
}
Handling Partial Failures #
async function fetchMultipleUsersWithFallback(userIds) {
const results = await Promise.allSettled(
userIds.map(id => fetchUser(id))
);
const users = [];
const errors = [];
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
users.push(result.value);
} else {
errors.push({ userId: userIds[index], error: result.reason });
}
});
return { users, errors };
}
// Usage
fetchMultipleUsersWithFallback([1, 2, 999, 4])
.then(({ users, errors }) => {
console.log('Successfully fetched users:', users);
console.log('Failed to fetch users:', errors);
});
Practical Examples #
Building a Simple API Client #
class ApiClient {
constructor(baseUrl) {
this.baseUrl = baseUrl;
}
async request(endpoint, options = {}) {
const url = `${this.baseUrl}${endpoint}`;
const config = {
headers: {
'Content-Type': 'application/json',
...options.headers
},
...options
};
if (config.body && typeof config.body === 'object') {
config.body = JSON.stringify(config.body);
}
try {
const response = await fetch(url, config);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error(`API request failed: ${error.message}`);
throw error;
}
}
async get(endpoint) {
return this.request(endpoint, { method: 'GET' });
}
async post(endpoint, data) {
return this.request(endpoint, {
method: 'POST',
body: data
});
}
async put(endpoint, data) {
return this.request(endpoint, {
method: 'PUT',
body: data
});
}
async delete(endpoint) {
return this.request(endpoint, { method: 'DELETE' });
}
}
// Usage
const api = new ApiClient('https://jsonplaceholder.typicode.com');
async function demo() {
try {
// Get a post
const post = await api.get('/posts/1');
console.log('Post:', post);
// Create a new post
const newPost = await api.post('/posts', {
title: 'My New Post',
body: 'This is the content',
userId: 1
});
console.log('Created post:', newPost);
// Update a post
const updatedPost = await api.put('/posts/1', {
title: 'Updated Title',
body: 'Updated content',
userId: 1
});
console.log('Updated post:', updatedPost);
} catch (error) {
console.error('Demo error:', error);
}
}
demo();
Implementing Retry Logic #
async function fetchWithRetry(url, options = {}, maxRetries = 3) {
for (let i = 0; i <= maxRetries; i++) {
try {
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
if (i === maxRetries) {
throw error;
}
console.log(`Attempt ${i + 1} failed, retrying...`);
// Wait before retrying (exponential backoff)
const delay = Math.pow(2, i) * 1000;
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
Best Practices #
1. Always Handle Errors #
// Bad
async function badExample() {
const data = await fetch('/api/data');
return data.json();
}
// Good
async function goodExample() {
try {
const response = await fetch('/api/data');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('Error fetching data:', error);
throw error;
}
}
2. Use Promise.all() for Concurrent Operations #
// Bad (sequential)
async function badConcurrency() {
const user = await fetchUser(1);
const posts = await fetchPosts(1);
const comments = await fetchComments(1);
return { user, posts, comments };
}
// Good (concurrent)
async function goodConcurrency() {
const [user, posts, comments] = await Promise.all([
fetchUser(1),
fetchPosts(1),
fetchComments(1)
]);
return { user, posts, comments };
}
3. Avoid Async/Await in Loops #
// Bad
async function badLoop(items) {
const results = [];
for (const item of items) {
const result = await processItem(item);
results.push(result);
}
return results;
}
// Good
async function goodLoop(items) {
const results = await Promise.all(
items.map(item => processItem(item))
);
return results;
}
Common Pitfalls and Solutions #
1. Not Handling Promise Rejections #
// This will cause an unhandled promise rejection
fetch('/api/data').then(response => response.json());
// Always add .catch() or use try/catch
fetch('/api/data')
.then(response => response.json())
.catch(error => console.error('Error:', error));
2. Mixing Promises and Async/Await #
// Inconsistent (mixing styles)
async function mixedStyle() {
const user = await fetchUser(1);
return fetchPosts(user.id)
.then(posts => posts.filter(post => post.published))
.catch(error => console.error(error));
}
// Consistent (use async/await throughout)
async function consistentStyle() {
try {
const user = await fetchUser(1);
const posts = await fetchPosts(user.id);
return posts.filter(post => post.published);
} catch (error) {
console.error(error);
}
}
Summary #
Asynchronous JavaScript is essential for modern web development. Key concepts include:
- Promises provide a clean way to handle asynchronous operations
- Async/await makes asynchronous code look and behave more like synchronous code
- Fetch API is the modern way to make HTTP requests
- Promise.all() enables concurrent execution of multiple async operations
- Error handling is crucial for robust asynchronous code
Practice these concepts to build responsive, efficient JavaScript applications!
Next Steps #
Tags
Related Tutorials
Best Way to Learn JavaScript: Common Questions Answered
Find answers to frequently asked questions about the best way to learn JavaScript, including timelines, resources, and effective study methods.
Last updated: Jan 11, 2025
DOM Manipulation with JavaScript - Complete Guide
Master DOM manipulation in JavaScript. Learn to select elements, modify content, handle events, and create dynamic web pages with practical examples.
ES6+ Features Every JavaScript Developer Should Know
Master modern JavaScript with ES6+ features including arrow functions, destructuring, modules, async/await, and more essential syntax improvements.
How to Turn On JavaScript: Complete Guide for All Browsers
Learn how to turn on JavaScript in Chrome, Firefox, Safari, and Edge with step-by-step instructions and troubleshooting tips.
Last updated: Jul 11, 2025