JavaScript Async/Await
JavaScript's async/await
syntax revolutionized how we handle asynchronous operations, making asynchronous code easier to write and understand. This powerful feature, introduced in ES2017 (ES8), builds on promises to provide a more intuitive way to work with asynchronous logic.
Introduction to Async/Await
Asynchronous operations like API calls, file operations, or timing functions can make code complex and difficult to follow. While promises improved on callback hell, they still required chaining .then()
calls, which could become unwieldy for complex operations.
async/await
offers a way to write asynchronous code that looks and behaves more like synchronous code, making it easier to understand and maintain.
Understanding Async Functions
An async function is a function declared with the async
keyword. This function automatically returns a Promise, and allows the use of the await
keyword inside it.
// Basic async function
async function fetchData() {
return "Data fetched successfully";
}
// This is equivalent to:
function fetchDataWithPromise() {
return Promise.resolve("Data fetched successfully");
}
When you call an async function, it returns a Promise that resolves with the value returned by the function:
fetchData().then(data => console.log(data));
// Output: "Data fetched successfully"
The Await Keyword
The real power of async functions comes with the await
keyword, which can only be used inside an async function. The await
expression pauses the execution of the async function until the Promise is resolved or rejected.
async function getUser() {
// This pauses execution until the promise resolves
let response = await fetch('https://api.example.com/user');
// Once the promise resolves, execution continues
let userData = await response.json();
return userData;
}
Without await
, the same code would look like this:
function getUserWithPromises() {
return fetch('https://api.example.com/user')
.then(response => response.json())
.then(userData => userData);
}
Error Handling with Async/Await
One of the major benefits of async/await is simplified error handling. Instead of chaining .catch()
methods, you can use familiar try/catch blocks:
async function getUser() {
try {
let response = await fetch('https://api.example.com/user');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
let userData = await response.json();
return userData;
} catch (error) {
console.error("Fetching user failed:", error);
throw error; // Re-throwing the error for the caller to handle
}
}
Practical Examples
Example 1: Sequential API Calls
When you need to make multiple API calls where each call depends on the result of the previous one:
async function getPostAndComments() {
try {
// Get a post
const postResponse = await fetch('https://api.example.com/posts/1');
const post = await postResponse.json();
// Use post ID to get comments
const commentsResponse = await fetch(`https://api.example.com/posts/${post.id}/comments`);
const comments = await commentsResponse.json();
return {
post,
comments
};
} catch (error) {
console.error("Failed to fetch post and comments:", error);
}
}
// Usage
getPostAndComments().then(data => {
console.log("Post title:", data.post.title);
console.log("Number of comments:", data.comments.length);
});
Example 2: Parallel API Calls with Promise.all
When you have multiple independent API calls you want to execute in parallel:
async function getUsersAndPosts() {
try {
// Start both fetch calls in parallel
const userPromise = fetch('https://api.example.com/users');
const postsPromise = fetch('https://api.example.com/posts');
// Wait for both to complete
const [userResponse, postsResponse] = await Promise.all([userPromise, postsPromise]);
// Process the results
const users = await userResponse.json();
const posts = await postsResponse.json();
return { users, posts };
} catch (error) {
console.error("Failed to fetch data:", error);
}
}
// Usage
getUsersAndPosts().then(data => {
console.log("Number of users:", data.users.length);
console.log("Number of posts:", data.posts.length);
});
Example 3: Building a Simple Loading State
A common use case in web applications is showing loading states while data is being fetched:
async function App() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
// Define the async function inside useEffect
async function fetchData() {
try {
setLoading(true);
const response = await fetch('https://api.example.com/data');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
setData(result);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}
// Call the async function
fetchData();
}, []);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
return (
<div>
{/* Render data here */}
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
);
}
Common Pitfalls and Best Practices
1. Forgetting to use await
Without await
, an async function will return a promise instead of the resolved value:
async function example() {
// Wrong - response is a Promise, not the actual response object
let response = fetch('https://api.example.com/data');
// Correct - waits for the Promise to resolve
let response = await fetch('https://api.example.com/data');
}
2. Awaiting inside loops
Awaiting inside a loop processes items sequentially, which may not be efficient:
// Sequential processing - slower
async function processItems(items) {
for (const item of items) {
await processItem(item); // Each item waits for previous to complete
}
}
// Parallel processing - faster
async function processItemsInParallel(items) {
const promises = items.map(item => processItem(item));
await Promise.all(promises); // All items processed in parallel
}
3. Error handling at different levels
It's important to handle errors at appropriate levels:
// Low-level function with specific error handling
async function fetchProduct(id) {
try {
const response = await fetch(`/api/products/${id}`);
if (!response.ok) throw new Error(`Product ${id} not found`);
return await response.json();
} catch (err) {
console.error(`Error fetching product ${id}:`, err);
throw err; // Re-throw for higher-level handlers
}
}
// Higher-level function with UI-related error handling
async function displayProductPage(id) {
try {
const product = await fetchProduct(id);
renderProductUI(product);
} catch (err) {
showErrorNotification("Couldn't load product. Please try again later.");
}
}
When to Use Async/Await vs. Regular Promises
Async/await is ideal for:
- Code that makes multiple sequential asynchronous calls
- When you need clearer error handling with try/catch
- When readability is a priority
Regular promises might be better when:
- You're dealing with simple, one-off asynchronous operations
- You need to create and return promises without awaiting them
- You're working in an environment that doesn't support async/await (although you can transpile)
Summary
async/await
provides a clean, intuitive way to work with asynchronous operations in JavaScript:
async
functions always return Promisesawait
pauses execution until a Promise resolves- Error handling uses familiar try/catch syntax
- Allows asynchronous code to be structured like synchronous code
- Works seamlessly with existing Promise-based APIs
By mastering async/await, you can write more maintainable asynchronous code that's easier to understand and debug.
Additional Resources
- MDN Web Docs: Async functions
- JavaScript.info: Async/await
- V8 Blog: Understanding JavaScript's async/await
Exercises
- Basic Conversion: Convert the following Promise chain to use async/await:
function getUser() {
return fetch('https://api.example.com/user')
.then(response => response.json())
.then(user => {
return fetch(`https://api.example.com/users/${user.id}/posts`);
})
.then(response => response.json())
.catch(error => console.error('Error:', error));
}
-
Error Handling: Create an async function that fetches data from an API and properly handles both network errors and API errors (where the response is not OK).
-
Parallel Operations: Write an async function that needs to fetch user data, user posts, and user comments in parallel (all independent of each other) and then combine them into a single object.
-
Timed Operations: Create an async function that simulates a delay using
setTimeout
wrapped in a Promise, then uses this delay between multiple operations.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)