async function (JavaScript)
Learn async function (JavaScript) step by step with clear examples and exercises.
Why This Matters
Async functions are a crucial part of modern JavaScript development, as they simplify handling asynchronous operations without the need for complex callbacks or promise chains. They enable cleaner and more readable code, making it easier to manage and maintain asynchronous tasks in your applications. Understanding async functions is essential for building efficient, scalable, and user-friendly web applications and server-side solutions.
Prerequisites
Before diving into async functions, you should have a solid understanding of the following topics:
- JavaScript basics: Variables, data types, operators, control structures, functions, and arrays.
- Callbacks: Functions passed as arguments to other functions to be executed later.
- Promises: Objects that represent the eventual completion or failure of an asynchronous operation and its resulting value.
- ES6 syntax: Constants (
const), let, arrow functions (=>), template literals (backticks\), and destructuring assignments. - Event Loop: Understanding how JavaScript handles multiple tasks concurrently using the event loop is important for understanding async functions' behavior.
Core Concept
Definition
An async function is a JavaScript function declared with the async keyword that can contain one or more await expressions. The await keyword pauses the execution of the async function until the promise it's waiting for resolves or rejects.
async function myAsyncFunction() {
// Your code here
}
How Async Functions Work
- When an async function is called, it immediately returns a Promise object. The Promise's initial state is
pending. - If the async function contains
awaitexpressions, the JavaScript engine will pause the execution of the function and wait for the promise to resolve or reject. - Once the awaited promise resolves, the resulting value is assigned to the variable on which the
awaitexpression was used. The async function continues executing from the line following theawait. - If an awaited promise is rejected, the async function will also be rejected with the same reason as the rejected promise.
- If there are no
awaitexpressions in the async function, it will immediately return a resolved Promise with the valueundefined. - The returned Promise can be handled using
.then(),.catch(), or other methods to handle its eventual resolution or rejection.
Example
Let's create an async function that fetches data from an API and logs it to the console:
const fetchData = async () => {
try {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
console.log(data);
} catch (error) {
console.error(error);
}
};
fetchData();
In this example, the fetchData function is an async arrow function that fetches JSON data from a URL and logs it to the console. It contains two await expressions: one for waiting for the fetch request to complete and another for parsing the response as JSON. The try-catch block is used to handle potential errors during the operation.
Parallel Execution with async/await
Async functions can be used to execute multiple asynchronous tasks concurrently using Promise.all(). This method returns a new Promise that resolves when all of the provided Promises have resolved or when one of them is rejected.
const fetchData1 = async () => {
// Fetch data from API 1
};
const fetchData2 = async () => {
// Fetch data from API 2
};
const fetchData3 = async () => {
// Fetch data from API 3
};
const fetchAllData = async () => {
try {
const [data1, data2, data3] = await Promise.all([fetchData1(), fetchData2(), fetchData3()]);
console.log(data1, data2, data3);
} catch (error) {
console.error(error);
}
};
fetchAllData();
In this example, we have three async functions fetchData1, fetchData2, and fetchData3 that fetch data from different APIs. The fetchAllData function uses Promise.all() to execute these functions concurrently and logs the results once all of them are resolved or one is rejected.
Worked Example
Problem
Write an async function that fetches user data from a mock API, validates the email address, checks if the user has an active subscription, and logs the user's name, email, and subscription status if all conditions are met.
const fetchUserData = async (userId) => {
// Fetch user data from the API
const response = await fetch(`https://api.example.com/users/${userId}`);
const userData = await response.json();
// Validate the email address and log an error if invalid
const isValidEmail = (email) => {
// Implement your email validation logic here
};
if (!isValidEmail(userData.email)) {
console.error('Invalid email address');
return;
}
// Check if the user has an active subscription
const checkSubscriptionStatus = async (userId) => {
// Fetch subscription status from API
const response = await fetch(`https://api.example.com/subscriptions/${userId}`);
const subscriptionData = await response.json();
return subscriptionData.active;
};
const hasActiveSubscription = await checkSubscriptionStatus(userData.id);
// Log the user's name, email, and subscription status if all conditions are met
if (hasActiveSubscription) {
console.log(`Name: ${userData.name}, Email: ${userData.email}, Subscription Status: Active`);
} else {
console.log(`Name: ${userData.name}, Email: ${userData.email}, Subscription Status: Inactive`);
}
};
// Call the function with a user ID
fetchUserData(123456);
In this worked example, we have an async function fetchUserData that fetches user data from an API and validates the email address. It also checks if the user has an active subscription using another async function called checkSubscriptionStatus. The final result includes the user's name, email, and subscription status.
Line-by-Line Walkthrough
- The
fetchUserDatafunction is declared as an async arrow function, taking auserIdparameter. - Inside the function, we fetch user data from the API using the provided userId and await for the response.
- We then await for the response to be parsed as JSON.
- After that, we've added a placeholder email validation function (
isValidEmail) that you can implement according to your needs. - If the email is valid, we check if the user has an active subscription using the
checkSubscriptionStatusasync function. - We await for the result of the
checkSubscriptionStatusfunction and store it in thehasActiveSubscriptionvariable. - Finally, we log the user's name, email, and subscription status based on the results.
- The function is called with a user ID at the end.
Common Mistakes
1. Forgetting the async keyword
Remember to declare your async functions using the async keyword. If you forget, the function will not return a Promise and will behave synchronously.
// Incorrect: missing async keyword
function myAsyncFunction() {
// Your code here
}
2. Using await outside an async function
You can only use await inside an async function. If you try to use it elsewhere, you'll get a syntax error.
// Incorrect: await used outside an async function
const myFunction = () => {
// Your code here
const result = await somePromise; // SyntaxError: await is only valid in async functions
};
3. Not handling rejected promises
If your async function contains await expressions, it's essential to handle rejected Promises to prevent unhandled promise rejections (UPRs). You can use a try-catch block or return the rejected Promise for proper error handling.
const myAsyncFunction = async () => {
try {
// Your code here
const result = await somePromiseThatMayReject();
// Continue with your code
} catch (error) {
console.error(error);
}
};
4. Mixing async/await with callbacks or traditional promise chains
While it's possible to mix async/await with callbacks or traditional promise chains, doing so can make your code more complex and harder to read. It's generally recommended to stick with one approach for better maintainability.
// Incorrect: mixing async/await with callbacks
const myFunction = async () => {
// Your code here
const result = await new Promise((resolve, reject) => {
// Callback-based logic here
// ...
resolve(someValue);
});
// Continue with your async/await code
};
5. Not using Promise.all() for parallel execution when appropriate
If you need to execute multiple asynchronous tasks concurrently, consider using Promise.all() instead of nested promises or callbacks for better readability and maintainability.
// Incorrect: nested promises
const fetchData1 = () => {
// Fetch data from API 1
};
const fetchData2 = () => {
// Fetch data from API 2
};
const fetchAllData = (callback) => {
Promise.all([fetchData1(), fetchData2()]).then((results) => {
callback(results[0], results[1]);
});
};
// Call the function with a callback
fetchAllData((data1, data2) => {
// Your code here
});
Practice Questions
- Write an async function that fetches user data from a mock API, validates the password, checks if the user has an active subscription, and logs the user's name, email, and subscription status if all conditions are met.
- Implement a simple async function that generates a random number between 1 and 100 using the
Math.random()method and waits for the result before logging it. - Write an async function that fetches data from multiple APIs, concatenates the results into a single string, and logs the final result.
- Implement a function to download a file asynchronously using the Fetch API and save it to the user's local storage.
- Write an async function that sends an email using a third-party API and logs the email status (sent or failed).
FAQ
1. What happens if there are no await expressions in an async function?
If there are no await expressions in an async function, it will immediately return a resolved Promise with the value undefined.
2. Can I use async/await with promises created using new Promise()?
Yes, you can use async/await with promises created using new Promise(). Just make sure to call await on the returned promise inside your async function.
3. What happens if an awaited promise is rejected and not handled within the async function?
If an awaited promise is rejected and not handled within the async function, it will propagate up the call stack as a rejected Promise. If no error handler catches it, JavaScript will throw an unhandled promise rejection (UPR).
4. Can I use async/await with native JavaScript Promises like fetch() or setTimeout()?
Yes, you can use async/await with native JavaScript Promises like fetch() and setTimeout(). Just make sure to call await on the returned promise inside your async function.
5. How does the event loop handle async functions with multiple await expressions?
The event loop handles async functions with multiple await expressions by pausing the execution of the function after each await expression until the corresponding promise resolves or rejects. Once a promise is resolved, the resulting value is assigned to the variable on which the await expression was used, and the function continues executing from the line following the await. If a promise is rejected, the async function will also be rejected with the same reason as the rejected promise.