In a recent project I worked on, I utilized Promises and Async/Await to fetch data from an API and display it on a web page. Promises are a fundamental part of asynchronous programming in JavaScript, and mastering them is crucial for any modern web developer.
In my opinion, understanding Promises should be a fundamental part of JavaScript basics, as they make it easier to write asynchronous code that is easier to read and maintain. In this article, we’ll take a deep dive into Promises and Async/Await, exploring how they work, their benefits, and how you can use them in your own JavaScript projects.
Asynchronous JavaScript
Asynchronous JavaScript refers to the practice of writing code that allows for non-blocking operations to occur. These operations may include things like fetching data from an API, updating a user interface, or reading and writing files. By allowing these operations to occur asynchronously, we can ensure that our applications remain responsive and don’t freeze up while they wait for a long-running operation to complete.
There are several ways to handle asynchronous operations in JavaScript, including callbacks, promises, and async/await. In this article, we’ll focus on promises and async/await, which are both more modern approaches to handling asynchronous code.
Promises
Promises are objects that represent the eventual completion or failure of an asynchronous operation and its resulting value. Promises are a way to deal with asynchronous code that provides a more intuitive and less error-prone way to handle callbacks.
Promises have three possible states:
- Pending: The initial state of a promise.
- Fulfilled: The state of a promise when the operation has been completed successfully.
- Rejected: The state of a promise when the operation has failed.
Promises have two primary methods that can be used to interact with them:
.then()
: This method is used to handle the fulfilled state of a promise. It takes a callback function as an argument, which is called when the promise is fulfilled..catch()
: This method is used to handle the rejected state of a promise. It takes a callback function as an argument, which is called when the promise is rejected.
Here’s an example of using a promise to fetch data from an API:
fetch('https://jsonplaceholder.typicode.com/todos/1')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error(error));
In this example, we’re using the fetch()
method to make an HTTP request to an API endpoint. The fetch()
method returns a promise that resolves with the response object. We use the .then()
method to handle the successful case, where the response is converted to JSON and logged to the console. We use the .catch()
method to handle the error case, where any errors are logged to the console.
Here is another example Loading external resources, in this case using the “Image” object
const loadImage = src => {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = reject;
img.src = src;
});
};
loadImage('https://example.com/image.jpg')
.then(img => console.log(`Image loaded: ${img.src}`))
.catch(error => console.error(`Error loading image: ${error}`));
This code creates a Promise that resolves to an Image
object once it has finished loading. We use the onload
and onerror
event handlers to resolve or reject the Promise, respectively. Once the Promise has been resolved, we log a message indicating that the image has loaded.
Another example is in chaining promises
fetch('https://api.example.com/users')
.then(response => response.json())
.then(userIds => {
const promises = userIds.map(userId => {
return fetch(`https://api.example.com/users/${userId}`)
.then(response => response.json());
});
return Promise.all(promises);
})
.then(users => console.log(users))
.catch(error => console.error(error));
This code first fetches a list of user IDs and then uses Promise.all()
to fetch user data for each ID in parallel. Once all the Promises have been resolved, we log the array of user data to the console.
Async/await
Async/await is a more recent approach to handling asynchronous code in JavaScript. It is built on top of promises and provides a more natural syntax for handling asynchronous code. Async/await makes asynchronous code look more like synchronous code, which can make it easier to read and write.
Here’s an example of using async/await to fetch data from an API:
async function fetchData() {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/todos/1');
const data = await response.json();
console.log(data);
} catch (error) {
console.error(error);
}
}
fetchData();
In this example, we define an async
function called fetchData
that uses await
to wait for the response to be returned from the API. We use a try...catch
block to handle any errors that may occur during the fetch.
Here is another example to Wait for multiple Promises using Promise.all()
const fetchAllData = async () => {
try {
const [userData, productData] = await Promise.all([
fetch('https://api.example.com/users').then(response => response.json()),
fetch('https://api.example.com/products').then(response => response.json())
]);
console.log(userData);
console.log(productData);
} catch (error) {
console.error(error);
}
};
fetchAllData();
This code uses Promise.all()
to wait for multiple Promises to resolve in parallel. We pass an array of Promises to Promise.all()
, and await
the result. Once both Promises have been resolved, we log the user and product data to the console.
Async/await provides a few benefits over Promises:
- Cleaner, more readable code: Async/await can make your code look more like synchronous code, which can make it easier to read and understand.
- Error handling: With promises, you need to use the
.catch()
method to handle errors. With async/await, you can use atry...catch
block, which is more intuitive and easier to reason about. - Sequential code: With async/await, you can write asynchronous code that looks and behaves like synchronous code
Resources I found helpful