Exploring Promises, Async Code, and Await in JavaScript
JavaScript's ability to handle asynchronous code is a powerful feature, but it can be challenging to understand at first. In this assignment, you will explore Promises, then(), catch(), finally(), and async/await. By the end, you'll have a solid grasp of these concepts and be able to use them confidently in your projects.
Understanding Promises
A Promise is an object that represents the eventual completion (or failure) of an asynchronous operation. Promises have three states:
- Pending: The initial state, neither fulfilled nor rejected.
- Fulfilled: The operation completed successfully.
- Rejected: The operation failed.
Here is a simple example of a Promise:
const promise = new Promise((resolve, reject) => {
const success = true; // Change to false to test rejection.
if (success) {
resolve("The operation was successful!");
} else {
reject("The operation failed.");
}
});
promise
.then((message) => {
console.log("Fulfilled:", message);
})
.catch((error) => {
console.error("Rejected:", error);
})
.finally(() => {
console.log("Promise has settled.");
});
Relating Promises to Try/Catch/Finally
Promises work similarly to the try/catch/finally block you may have used for synchronous code. The following example demonstrates the similarity with working code, only the try and finally blocks will execute. Change the code within the try block to broken JavaScript to see how it affects the output:
try {
// Synchronous code that might throw an error
const result = "Success!";
console.log("Result:", result);
} catch (error) {
// "Catch" (handle) any errors that occur
console.error("Error:", error);
} finally {
// Code that runs "finally", regardless of success or failure
console.log("Operation complete.");
}
Chaining Promises with then()
Promises can be chained using then() to handle sequences of asynchronous tasks:
const step1 = () => {
return new Promise((resolve) => {
console.log("Step 1 completed.");
resolve("Step 1 result");
});
};
const step2 = (prevResult) => {
return new Promise((resolve) => {
console.log("Step 2 completed with:", prevResult);
resolve("Step 2 result");
});
};
step1()
.then(step2)
.then((finalResult) => {
console.log("All steps completed. Final result:", finalResult);
});
Try It
Save the above code in a JavaScript file, practice.js for example, and run the file with this command node practice.js or copy and paste the above code into your browsers console, and observe the output. Notice how each step depends on the previous one.
Handling Errors with catch()
Promises let you handle errors using catch(). In the example below, notice the additional parameter in our promise function, reject. A complete promise function should include both resolve() and reject(), calling the appropriate one to indicate success or failure. It is common practice to declare promises with abbreviated parameter names like this: new Promise((res, rej) => { ... }). If you run the following code it should log the error message from our reject(...) and then the log message from the finally() block:
const failingStep = () => {
return new Promise((resolve, reject) => {
reject("Something went wrong!");
});
};
failingStep()
.then((result) => {
console.log("This will not run:", result);
})
.catch((error) => {
console.error("Caught an error:", error);
})
.finally(() => {
console.log("Execution finished.");
});
Understanding Async/Await
Consider asynchronous programming as similar to streaming Netflix while downloading a large video game. You want to ensure that your Netflix stream continues smoothly without interruptions from the game download. This is where async code becomes essential.
The async keyword creates a function that handles these parallel tasks, automatically wrapping everything in Promises. The await keyword lets you pause and wait for those Promises to resolve, just like you learned earlier.
Here is how Promises looked before:
// Old way with Promises
fetchUserData()
.then(user => fetchUserPosts(user))
.then(posts => {
console.log(posts);
})
.catch(error => {
console.log("Error:", error);
});
Now here is the same code with async/await instead:
// New way with async/await
async function getUserPosts() {
try {
const user = await fetchUserData();
const posts = await fetchUserPosts(user);
console.log(posts);
} catch (error) {
console.log("Error:", error);
}
}
Real-World Example: Spotify API Call
Let's look at something practical like fetching a playlist from Spotify:
const getPlaylist = async () => {
try {
console.log("Loading playlist...");
const playlist = await spotifyAPI.getPlaylist('top_hits');
const tracks = await spotifyAPI.getPlaylistTracks(playlist.id);
return tracks;
} catch (error) {
console.log("Couldn't load playlist:", error);
}
};
// Using it is simple:
getPlaylist().then((tracks) => {
console.log(`Here are the playlist tracks!\\n${tracks}`);
});
Key Points
asyncfunctions always return a Promiseawaitonly works insideasyncfunctions- You will have to use
try/catchfor error handling withinasyncfunctions
Practical Exercise
Let's create a simulated food ordering system to practice working with Promises and async/await. Create a new JavaScript file called food-court.js and add the following code to it:
// Simulates getting menu data
function getMenu() {
return new Promise((resolve) => {
setTimeout(() => {
resolve([
'Burger',
'Pizza',
'Tacos',
'Sushi'
]);
}, 1500);
});
}
// Simulates placing an order
function placeOrder(food) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (food) {
resolve(`Your ${food} order has been placed!`);
} else {
reject('Please select a food item!');
}
}, 2000);
});
}
// Simulates order preparation
function prepareOrder(order) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (order.includes('placed')) {
resolve('Your food is ready for pickup! 🍔');
} else {
reject('Something went wrong with your order!');
}
}, 3000);
});
}
// Using Promises (the traditional way)
function orderFoodWithPromises() {
console.log('Starting your order (Promise version)...');
getMenu()
.then(menu => {
console.log('Menu items:', menu);
return placeOrder(menu[0]);
})
.then(orderMessage => {
console.log(orderMessage);
return prepareOrder(orderMessage);
})
.then(ready => {
console.log(ready);
})
.catch(error => {
console.log('Error:', error);
})
.finally(() => {
console.log('Order process complete. (Simulation Competed)');
});
}
// Using async/await (the modern way)
async function orderFoodWithAsync() {
try {
console.log('Starting your order (async/await version)...');
const menu = await getMenu();
console.log('Menu items:', menu);
const orderMessage = await placeOrder(menu[0]);
console.log(orderMessage);
const ready = await prepareOrder(orderMessage);
console.log(ready);
} catch (error) {
console.log('Error:', error);
} finally {
console.log('Order process complete. (Simulation Competed)');
}
}
// Try both versions!
orderFoodWithPromises();
// orderFoodWithAsync();
Now run this code using the Node.js command node food-court.js or if you want, copy paste the code into your browser's console. You can comment out one of the function calls to compare the Promise-based version with the async/await version.
This example demonstrates real-world async operations using timeouts to simulate network requests. Each function returns a Promise, and you can see how much cleaner the async/await version looks compared to the Promise chains.
Experimentation Ideas
- Try changing the food item being ordered (use different array indexes)
- Pass
nulltoplaceOrder()to see error handling in action - Modify the timeouts to see how timing affects the process
- Add a new step to the process (like delivery or payment)
- If you're really adventurous, make this an interactive application!
Looking Ahead: Building with Express
The async patterns we have covered today form the foundation of modern web development. In our upcoming assignments, we will apply these concepts as we dive into Express, a powerful Node.js framework for building web applications.
The connection is direct: When building web servers with Express, you'll handle multiple concurrent requests using the same asynchronous patterns we just learned. Your server will need to process multiple user interactions simultaneously while maintaining smooth performance. For example, just as we waited for our simulated food orders to process, your Express routes will handle user interactions and database operations. The async/await syntax you learned today will help you manage these operations cleanly and effectively, ensuring your web applications remain responsive even under heavy user load.