Skip to main content

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.

javascript
// 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:

javascript
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.

javascript
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:

javascript
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:

javascript
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:

javascript
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:

javascript
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:

javascript
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:

javascript
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:

javascript
// 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:

javascript
// 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 Promises
  • await 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

Exercises

  1. Basic Conversion: Convert the following Promise chain to use async/await:
javascript
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));
}
  1. 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).

  2. 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.

  3. 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! :)