Learn how to simplify your JavaScript promise chains and understand asynchronous programming with async/await through this comprehensive tutorial. By the end of this guide, you'll know how to effectively refactor your code using async/await to improve readability, manage errors more efficiently, and handle parallel asynchronous tasks.
What are Async and Await in JavaScript?
Async and await are modern JavaScript features introduced in ES2017 (ES8) that simplify working with asynchronous code, making it look and behave like synchronous code. They are built on top of JavaScript promises but provide a more straightforward syntax.
- Async functions: An
asyncfunction is a special type of function that always returns a promise and can use theawaitkeyword to pause the execution of code until a promise is resolved or rejected. - The
awaitkeyword: Inside anasyncfunction, theawaitoperator pauses its execution until the promise fulfills, allowing other code to run in the meantime.
These features reduce the complexity of chaining .then() calls in promises and make your code easier to read and maintain.
prerequisites
To follow along with this tutorial, you will need:
- A basic understanding of JavaScript promises.
- Familiarity with functions and using try/catch statements for error handling.
- A modern JavaScript runtime like Node.js (16+) or any browser that supports ES6+ features.
Setting Up a Coding Example
Let's build a simple example to demonstrate the traditional promise-based syntax and how it can be refactored with async/await.
Example Setup
We define two asynchronous functions using promises:
function makeRequest(location) {
return new Promise((resolve, reject) => {
console.log(`Making request to ${location}`);
if (location === "Google") {
resolve("Google says hi");
} else {
reject("We can only talk to Google");
}
});
}
function processRequest(response) {
return new Promise((resolve) => {
console.log("Processing response");
resolve(`${response} + extra information`);
});
}In this example:
makeRequest(location)simulates an HTTP request and resolves if the location is "Google".processRequest(response)processes the response by appending additional information.
We'll use this setup for both promise chaining and async/await.
Step-by-Step Guide to Refactor Code with Async/Await
Let's start by seeing how the code looks with promises and then refactor it to use async/await.
Promise-Based Code
function makeRequest(location) {
return new Promise((resolve, reject) => {
console.log(`Making request to ${location}`);
if (location === "Google") {
resolve("Google says hi");
} else {
reject("We can only talk to Google");
}
});
}
function processRequest(response) {
return new Promise((resolve) => {
console.log("Processing response");
resolve(`${response} + extra information`);
});
}
makeRequest("Google")
.then(response => {
console.log("Response received");
return processRequest(response);
})
.then(processedResponse => {
console.log(processedResponse);
})
.catch(error => {
console.error(error);
});This code works but can become harder to read with nested .then() calls. Now, let's refactor it.
Async/Await-Based Code
To use async/await, you must wrap the code in an async function:
function makeRequest(location) {
return new Promise((resolve, reject) => {
console.log(`Making request to ${location}`);
if (location === "Google") {
resolve("Google says hi");
} else {
reject("We can only talk to Google");
}
});
}
function processRequest(response) {
return new Promise((resolve) => {
console.log("Processing response");
resolve(`${response} + extra information`);
});
}
async function doWork() {
try {
const response = await makeRequest("Google");
console.log("Response received");
const processedResponse = await processRequest(response);
console.log(processedResponse);
} catch (error) {
console.error(error);
}
}
doWork();Key improvements with async/await:
- Code reads top-down, resembling synchronous programming.
- Error handling is more structured with a single
try/catchblock. - Easier to manage intermediate variables (
response,processedResponse).
Using Promise.all with Async/Await
When multiple asynchronous operations are independent, you can use Promise.all for concurrent execution.
comparison
Sequential Execution with await:
function makeRequest(location) {
return new Promise((resolve, reject) => {
console.log(`Making request to ${location}`);
if (location === "Google") {
resolve("Google says hi");
} else {
reject("We can only talk to Google");
}
});
}
function processRequest(response) {
return new Promise((resolve) => {
console.log("Processing response");
resolve(`${response} + extra information`);
});
}
async function processInSequence() {
const result1 = await makeRequest("Google");
const result2 = await processRequest(result1);
console.log(result2);
}
processInSequence();Parallel Execution with Promise.all:
function makeRequest(location) {
return new Promise((resolve, reject) => {
console.log(`Making request to ${location}`);
if (location === "Google") {
resolve("Google says hi");
} else {
reject("We can only talk to Google");
}
});
}
function processRequest(response) {
return new Promise((resolve) => {
console.log("Processing response");
resolve(`${response} + extra information`);
});
}
async function processInParallel() {
const [result1, result2] = await Promise.all([
makeRequest("Google"),
processRequest("Some Data")
]);
console.log(result1, result2);
}
processInParallel();Using Promise.all improves performance by running both tasks at the same time. However, ensure proper error handling during parallel execution.
Top-Level Await for Simplicity
With ECMAScript 2022, JavaScript now supports top-level await. This means you can use await directly at the top level of a module without wrapping it inside a function.
Example with top-level await:
function makeRequest(location) {
return new Promise((resolve, reject) => {
console.log(`Making request to ${location}`);
if (location === "Google") {
resolve("Google says hi");
} else {
reject("We can only talk to Google");
}
});
}
function processRequest(response) {
return new Promise((resolve) => {
console.log("Processing response");
resolve(`${response} + extra information`);
});
}
const response = await makeRequest("Google");
console.log("Response received");
const processedResponse = await processRequest(response);
console.log(processedResponse);Top-level await simplifies one-off asynchronous operations, but it may not be suitable for complex workflows where multiple functions collaborate.
FAQ
What does async do in JavaScript async await?
The async keyword enables a function to operate asynchronously and always return a promise. Inside an async function, you can use the await keyword to pause execution until a promise is resolved.
How is async/await different from promises?
Async/await simplifies the syntax for working with promises. Instead of chaining .then() methods, async/await allows asynchronous code to be written in a cleaner, more synchronous style.
How do I handle errors with async/await?
Wrap your async/await code in a try/catch block. This allows you to handle errors in a consistent and streamlined way, similar to traditional exception handling in synchronous code.
Official reference: MDN JavaScript documentation.