Async/await simplifies asynchronous programming by allowing developers to write code that looks and behaves like synchronous code, improving readability and understandability.
This page is a follow-up branch of the in-depth article "Async in JS: How does it work", if you'd like to learn about the history of asynchronous JavaScript and its evolution, refer to it.
In a nutshell
When you use the async
keyword before a function definition, it indicates that the function will be asynchronous. This means that the function is designed to work with asynchronous operations, such as network requests or reading files, without blocking the main execution thread. In JavaScript, asynchronous functions are usually implemented using promises.
As a result, when you declare a function as async
, the return value of that function is automatically wrapped in a Promise. Even if the return value is not a promise itself, JavaScript will create a promise that resolves to that value.
Here's an example:
// An asynchronous function
async function getUserName(userId) {
// Simulate an asynchronous operation (e.g., fetching data from a server)
const simulatedData = {
1: "Alice",
2: "Bob",
3: "Charlie",
};
return simulatedData[userId];
}
// Usage
getUserName(1).then((name) => {
console.log(`User 1: ${name}`); // Output: User 1: Alice
});
In this example, the getUserName
function is declared as async
. Even though it returns a simple value from the simulatedData
object, the return value is automatically wrapped in a promise. When we call the getUserName
function, we can use the then
method to handle the resolved value, just like we would with any other promise.
Asynchronous functions are essential when dealing with operations that take an unpredictable amount of time to complete, such as making API calls, interacting with databases, or reading files. These operations are not executed immediately; instead, the code sends a request and waits for the response.
The await
keyword is used inside an async
function to pause the execution of the function until the asynchronous operation is completed. It allows you to write code that appears synchronous, even though it's performing asynchronous tasks. This makes the code more readable and easier to understand.
// Import the 'fs' module to work with the file system
const fs = require('fs').promises;
// An async function to read a file
async function readFileContent(filePath) {
try {
// Use the 'await' keyword to wait for the file to be read
const content = await fs.readFile(filePath, 'utf-8');
console.log(`File content: ${content}`);
} catch (error) {
console.error(`Error reading the file: ${error.message}`);
}
}
// Usage
readFileContent('./example.txt');
In this example, we're using the fs.promises
module to read a file. The readFile
function is asynchronous, so it returns a promise. Inside the readFileContent
async function, we use the await
keyword to wait for the fs.readFile
promise to resolve. Once the promise resolves, the file's content is logged to the console. If there's an error reading the file, the catch block logs the error message.
Call chains vs async/await
The async/await keywords in JavaScript don't introduce new functionality; they simply make it easier to work with promises. They allow you to write cleaner and more intuitive code compared to using chained then
and catch
callbacks.
Here's an example illustrating the difference between using promises with a call chain and using async/await.
Using a call chain with promises:
function fetchData() {
return new Promise((resolve) => {
setTimeout(() => {
resolve("Data fetched");
}, 1000);
});
}
function processData(data) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(`${data} and processed`);
}, 1000);
});
}
// Chained promises with then and catch
fetchData()
.then((data) => {
return processData(data);
})
.then((result) => {
console.log(result); // Output: Data fetched and processed
})
.catch((error) => {
console.error(`Error: ${error}`);
});
Using async/await:
async function fetchDataAndProcess() {
try {
const data = await fetchData();
const result = await processData(data);
console.log(result); // Output: Data fetched and processed
} catch (error) {
console.error(`Error: ${error}`);
}
}
// Call the async function
fetchDataAndProcess();
In both examples, we fetch and process data using promises. The first example uses a call chain with then
and catch
, while the second example uses the async/await keywords. The async/await version is easier to understand because it looks more like synchronous code. It's also more straightforward to handle errors using try/catch blocks, making it a popular choice for developers working with asynchronous operations.
Tips & best practices
Below you'll find some personal tips but also industry best practices when working with async/await.
Always use async/await instead of then() chains and callbacks.
- Async/await is easier to read, debug, and manage compared to chaining multiple
then()
andcatch()
functions or using callback functions.
- Async/await is easier to read, debug, and manage compared to chaining multiple
Await should not be used outside of asynchronous functions.
- The
await
keyword can only be used inside an async function. If you want to perform an asynchronous operation in the global scope, you must usethen()
.
- The
Use
await Promise.all([...])
to execute several independent asynchronous functions in parallel.- Instead of waiting for one asynchronous function to complete before executing the next, you can run multiple independent async functions concurrently using
Promise.all()
. This can help improve the overall performance of your code.
- Instead of waiting for one asynchronous function to complete before executing the next, you can run multiple independent async functions concurrently using
Example:
async function getUser() {
// Returns the information about the user
}
async function getNews() {
// Returns the list of news
}
// Using Promise.all to run getUser() and getNews() concurrently
const [user, news] = await Promise.all([getUser(), getNews()]);
Don't mix async/await and Promise.then syntax.
- To maintain consistency and readability in your code, try to stick to one approach throughout your project: either async/await or Promise.then. Mixing the two can make your code harder to read and maintain.
Use try/catch blocks for error handling.
- When using async/await, it's a good practice to handle errors with try/catch blocks. This approach is more consistent and easier to understand compared to handling errors with Promise.catch().
Example:
async function fetchData() {
try {
const data = await fetchSomeData();
console.log("Data fetched:", data);
} catch (error) {
console.error("Error fetching data:", error);
}
}
Avoid "async/await hell."
- Similar to "callback hell," "async/await hell" refers to a situation where you have multiple nested async functions, making the code hard to read and maintain. Instead, try to break down your code into smaller, reusable async functions.
Keep your asynchronous functions focused.
- Try to create asynchronous functions that perform a single, well-defined task. This approach makes your code easier to understand, test, and maintain.
Use
Promise.race([...])
when you need the result of the fastest promise.- If you have several asynchronous operations and need to get the result of the one that completes first, use
Promise.race()
. It returns a promise that resolves or rejects as soon as one of the input promises settles.
- If you have several asynchronous operations and need to get the result of the one that completes first, use
Example:
async function fetchFromAPI_A() {
// Fetch data from API A
}
async function fetchFromAPI_B() {
// Fetch data from API B
}
// Using Promise.race to get the result from the fastest API
const fastestResult = await Promise.race([fetchFromAPI_A(), fetchFromAPI_B()]);
By adhering to these recommendations, you can improve the readability, maintainability, and performance of your JavaScript code when working with asynchronous operations using async/await and promises.