Imagine your code is sitting by the door, waiting for an asynchronous event to happen, like a kid waiting for their birthday party to start. It’s a waste of time, and frankly, it’s not how we build efficient applications. Instead, we want to embrace the concept of asynchronous programming, allowing our code to get on with its tasks while waiting for other operations to complete.
In JavaScript, we often deal with operations that take time, such as fetching resources over the network. Rather than blocking the execution of our code, we can utilize callbacks, promises, or async/await syntax to handle these operations gracefully.
function fetchData(url) { return new Promise((resolve, reject) => { fetch(url) .then(response => { if (!response.ok) { throw new Error('Network response was not ok'); } return response.json(); }) .then(data => resolve(data)) .catch(error => reject(error)); }); }
Here’s how we can use a promise to handle data fetching. We’re allowing our code to proceed while waiting for the network response. This approach avoids the dreaded callback hell and keeps our code clean. Instead of waiting by the door, we can go do something productive while we wait for the delivery.
Samsung T7 Portable SSD, 2TB External Solid State Drive, Speeds Up to 1,050MB/s, USB 3.2 Gen 2, Reliable Storage for Gaming, Students, Professionals, MU-PC2T0T/AM, Gray
35% OffBut we need to make sure that when the data arrives, we handle it properly without creating a mess. This is where structured error handling becomes crucial. Let’s enhance our example by adding some error handling.
fetchData('https://api.example.com/data') .then(data => { console.log('Data received:', data); }) .catch(error => { console.error('Error fetching data:', error); });
With this in place, we’re not just passively waiting. We are prepared for both success and failure. If the data arrives, we log it; if there’s an issue, we catch the error and log that as well. This is how we can create resilient applications that don’t fall apart when something goes wrong.
Now that we’ve established a method for handling async operations, we can consider about how to open that package when it arrives. Just like after ripping into a gift, we’ll focus on what happens next…
How to actually open the package when it arrives
To effectively open the package, we need to ensure that the operation we want to perform after receiving the data is clearly defined and executed. Often, we want to manipulate the received data and then update the UI accordingly. That is where the concept of chaining comes into play.
By using the promise returned from our asynchronous function, we can chain multiple operations together, ensuring that they happen in the correct order. Here’s an example of how we can do that:
fetchData('https://api.example.com/data') .then(data => { // Process the data const processedData = data.map(item => item.value); return processedData; }) .then(processedData => { // Update the UI with the processed data updateUI(processedData); }) .catch(error => { console.error('Error fetching or processing data:', error); });
In this example, we first fetch the data, then process it to extract the specific values we’re interested in, and finally update the UI with that processed data. Each step is clearly defined, and any error that occurs in the chain will be caught in the catch
block at the end.
Now, if you’re using async/await, the code can be made even more readable. Instead of chaining promises, we can use the await
keyword to pause execution until the promise resolves:
async function loadData() { try { const data = await fetchData('https://api.example.com/data'); const processedData = data.map(item => item.value); updateUI(processedData); } catch (error) { console.error('Error fetching or processing data:', error); } }
In this async function, the code flows in a linear fashion, making it easier to follow and understand. We’re still handling errors appropriately, but the syntax is cleaner and more intuitive.
As we navigate through asynchronous operations, it’s essential to maintain a clear understanding of the order in which things happen. This not only helps in keeping our code organized but also ensures that we don’t lose track of what’s going on. When we’re dealing with UI updates, for example, we want to make sure that we’re only trying to update the interface after the data has been fully processed and is ready to be displayed.
Now, let’s delve into ensuring that one thing happens after another without losing our minds amidst all these asynchronous calls. This is where we’ll introduce some strategies…
Making one thing happen after another without losing your mind
So you’ve got one asynchronous thing happening. Great. But in the real world, it’s rarely just one thing. It’s a cascade. You fetch a list of users, then you need to fetch the details for the first user, and then maybe you need to fetch their recent activity. Each step depends on the one before it. If you’re not careful, you end up right back in that “pyramid of doom” we thought we escaped, even with Promises.
Chaining .then()
blocks is the fundamental way Promises handle this. Each .then()
can return a new Promise, and the next .then()
in the chain will wait for it to resolve. It’s a perfectly workable system for ensuring A happens, then B, then C.
function processUserFlow(userId) { fetchUserDetails(userId) .then(details => { console.log('Got details:', details); // This next call also returns a promise return fetchUserPosts(details.id); }) .then(posts => { console.log('Got posts:', posts); // And another one return submitAnalytics(posts.length); }) .then(analyticsResult => { console.log('Analytics sent:', analyticsResult); }) .catch(err => { // A single catch handles errors from any point in the chain console.error('User processing failed:', err); }); }
This works, but let’s be honest, it’s a bit clunky. You have all these nested scopes and return statements just to pass data along. The real breakthrough for writing sequential asynchronous code that doesn’t make your eyes bleed is async/await
. It’s just syntactic sugar over Promises, but it’s the best kind of sugar—the kind that makes your code healthier and more readable. Let’s rewrite that same flow.
async function processUserFlow(userId) { try { const details = await fetchUserDetails(userId); console.log('Got details:', details); const posts = await fetchUserPosts(details.id); console.log('Got posts:', posts); const analyticsResult = await submitAnalytics(posts.length); console.log('Analytics sent:', analyticsResult); } catch (err) { console.error('User processing failed:', err); } }
Look at that. It reads like a simple, synchronous script. There are no nested callbacks, no .then()
chains. The await
keyword effectively pauses the function until the Promise resolves, and then assigns the result to a variable. The logic is identical to the Promise chain, but the code is infinitely easier for a human to follow. That is how you make one thing happen after another without losing your mind.
But what if the operations don’t depend on each other? What if you need to fetch user details, their company information, and their product subscriptions all simultaneously to display on a dashboard? Running them sequentially using await
would be needlessly slow. You’d be waiting for the user details to come back before you even *start* asking for the company info. That’s like sending three letters by the same mail carrier, telling him to deliver the first one and return before he even takes the second one.
That is where Promise.all()
comes in. It takes an array of promises and gives you back a single promise that resolves when all of the input promises have resolved. You fire off all your requests at the same time and just wait for the whole batch to finish. That is parallelism, and it’s a huge performance win.
async function loadDashboard(userId) { try { // Fire all requests in parallel const [details, company, subscriptions] = await Promise.all([ fetchUserDetails(userId), fetchCompanyInfo(userId), fetchSubscriptions(userId) ]); // Once all data is here, render the UI renderDashboardUI(details, company, subscriptions); } catch (err) { // If any of the promises fail, Promise.all rejects. console.error('Failed to load dashboard:', err); showErrorUI(); } }
Using Promise.all()
with async/await
and destructuring assignment is a powerful and clean pattern for managing concurrent, independent operations. The important thing to remember is that if any single promise in the array rejects, the entire Promise.all()
call will reject immediately. This is usually the behavior you want—if you can’t load a critical piece of the dashboard, you should probably show an error state rather than a broken, half-loaded page.
Source: https://www.jsfaq.com/how-to-consume-a-promise-using-then-in-javascript/