Hello again! It's great to see you back.
In our last lesson, we dug into JavaScript Promises and learned how to manage asynchronous outcomes using .then() for successes and .catch() for failures. This pattern, especially with chaining, is a massive improvement over old-school callbacks.
Today, we're going to build directly on that knowledge. We'll explore a more modern and intuitive syntax for working with Promises: async/await. The goal of this lesson is to learn how to write cleaner, more readable asynchronous code that accomplishes the same thing as .then() chains, but often in a more straightforward way. You'll find this syntax makes asynchronous code look and behave much more like the synchronous code you might be used to from languages like Python.
From Promise Chains to async/await
Let's start by looking at a typical Promise chain, similar to what we saw in the last lesson. Imagine we have two functions that return promises: makeRequest and processRequest.
// A typical .then() chain
makeRequest('Google')
.then(response => {
console.log('Response Received');
return processRequest(response); // This also returns a promise
})
.then(processedResponse => {
console.log(processedResponse);
})
.catch(err => {
console.error(err);
});
This code works perfectly well, but the nesting and callbacks can start to get a bit cumbersome as logic gets more complex. This is where async/await comes in. It's often described as "syntactic sugar" over Promises, meaning it's a different way to write the same logic, but it doesn't change how things work underneath.
Let's watch a video that perfectly illustrates how to convert the code above into the async/await style.
The video 'JavaScript Async Await' from Web Dev Simplified provides a fantastic side-by-side comparison of promise chains and async/await. It clearly demonstrates the syntax and the benefits.
Please watch from the beginning up to 5:19. The first part (until 2:45) shows the .then() approach, which will be a good recap. The second part (from 2:45 onwards) shows the conversion to async/await. Focus on how the code becomes flatter and more sequential.
The Two Core Keywords: async and await
As you saw in the video, async/await revolves around two keywords. Let's break down their roles:
-
async: This keyword is placed before a function declaration. It does two critical things:- It enables the use of the
awaitkeyword inside that function. - It automatically ensures that the function returns a Promise. If you explicitly return a value (e.g.,
return 42), theasyncfunction will wrap it in a fulfilled Promise for you.
- It enables the use of the
-
await: This keyword can only be used inside anasyncfunction. When you placeawaitin front of an expression that returns a Promise (likefetch()or anotherasyncfunction), it does the following:- It "pauses" the execution of the
asyncfunction until the Promise settles. - If the Promise fulfills,
awaitreturns the fulfilled value. - If the Promise rejects, it throws an error, which we'll need to handle.
- It "pauses" the execution of the
Here is the code from the video, rewritten to look like this:
// The same logic with async/await
async function doWork() {
const response = await makeRequest('Google');
console.log('Response Received');
const processedResponse = await processRequest(response);
console.log(processedResponse);
}
Notice how the code reads from top to bottom, without any .then() callbacks. The await keyword effectively "unpacks" the value from the promise for you.
Handling Errors with try...catch
So, await gives us the successful value, but what about errors? In .then() chains, we use .catch(). With async/await, we use a construct you might already be familiar with from other programming languages: the try...catch block.
- You wrap your "happy path" code—the code that might throw an error—inside the
try { ... }block. - You write your error-handling logic inside the
catch (error) { ... }block.
If any await-ed promise in the try block rejects, the execution immediately jumps to the catch block, where you can handle the error.
Let's continue with the same video to see exactly how this works.
This next segment shows how to add error handling to our async function using a try...catch block.
Watch from 5:19 to 6:24. Observe how the try block contains the core logic and how the catch block gracefully handles the rejected promise.
Here's our complete function with robust error handling:
async function doWork() {
try {
const response = await makeRequest('Facebook'); // This will fail
console.log('Response Received');
const processedResponse = await processRequest(response);
console.log(processedResponse);
} catch (err) {
// If makeRequest() rejects, the code jumps here
console.error(err);
}
}
doWork(); // Don't forget to call the function!
This pattern is the standard for handling errors with async/await and is considered very readable because it clearly separates the success logic from the error-handling logic.
A Practical Example: async/await with fetch
Let's see this in the context of a real-world task: fetching data from an API. In the last lesson, we used fetch with .then() chains. Now, look how clean it becomes with async/await.

Let's break down the code in the image:
- We define
getDataas anasyncfunction. await fetch(...)pauses the function until the network request is complete and theResponseobject is available.await response.json()pauses the function again until the response body is fully parsed into a JavaScript object.- Finally, we can work with the
dataas a regular synchronous variable.
For another excellent walkthrough of this entire concept, from the motivation to a real fetch example, the following video is a great resource.
Async Await vs. Promises - JavaScript Tutorial for beginners
The video 'Async Await vs. Promises' from ColorCode offers another great perspective, emphasizing the improved readability and providing a clear fetch example.
You can watch this video to reinforce the concepts. The most relevant parts are the motivation (0:00-2:38), the syntax introduction (2:38-7:08), the fetch example (7:08-10:53), and the try/catch explanation (14:13-18:40).
Test your understanding!
In the previous lesson, you worked with this function:
function createDelayedMessage(shouldSucceed) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (shouldSucceed) {
resolve("Success: The message has arrived!");
} else {
reject(new Error("Failure: The message was lost."));
}
}, 1000);
});
}
Your task is to write a new async function called handleMessage. This function should:
- Call
createDelayedMessage(true). awaitthe result.- Log the successful result to the console.
- Wrap the logic in a
try...catchblock to handle potential errors, logging any error message toconsole.error(). - Finally, call your
handleMessage()function to run it.
How would you write this function? What would happen if you changed the call to createDelayedMessage(false)?
Show answer
Here is the implementation:
async function handleMessage() {
try {
// Set shouldSucceed to true or false to test both paths
const message = await createDelayedMessage(true);
console.log(message);
} catch (error) {
console.error(error.message);
}
}
// Run the function
handleMessage();
If called with createDelayedMessage(true), the output after 1 second will be:Success: The message has arrived!
If you change it to createDelayedMessage(false), the promise will reject, the catch block will execute, and the output after 1 second will be:Failure: The message was lost.
Conclusion
You have now learned the modern, preferred way to handle asynchronous operations in JavaScript. While it's crucial to understand the Promises and .then() chains working underneath, async/await will be your primary tool for writing clean, maintainable, and less error-prone asynchronous code.
Key Takeaways:
async/awaitis syntactic sugar over Promises, making asynchronous code read like synchronous code.- The
asynckeyword declares a function that returns a Promise and allows the use ofawaitwithin it. - The
awaitkeyword pauses function execution until a Promise settles, and then returns its fulfilled value or throws an error if it rejects. - Error handling in
asyncfunctions is done using standardtry...catchblocks, which cleanly separates success and failure logic.
In our next lesson, we will dive deeper into the fetch() API. You've seen it used as an example here, but we will formally cover how to use it to get data from external REST APIs and handle different kinds of JSON responses—a cornerstone skill for any web developer.