Refactoring Promise Chains w/Async-Await

Brian Lego
Jan 10 · 4 min read

In my never ending journey of refactoring and refining my code my most recent endeavor has been in writing cleaner and easier to read code with the help of async-await

Image for post
Image for post

Up until recently I had only used JavaScript & Promises primarily within the context of writing React code. As far as asynchronous code and Promises go, the only use-cases I had for them were in writing fetch() calls to an API. It would usually look something like this:

function getUsers(){
fetch('https://some-url-here.com/')
.then(resp => resp.json()
.then(data => someFunctionToUseReturnedData(data))
.catch(err => console.log(err))
}

Anyone familiar with the code above knows that JavaScript’s fetch API makes use of Promises& by chaining subsequent Promises together you achieve asynchronous functionality. Calling fetch() returns a Promise, as does any subsequent .then() or .catch() call, which then each have to handled. The ‘lifecycle’ of a Promise is show below for reference if you need a quick refresher:

Image for post
Image for post
Promise lifecycle

A simple API call like this is easy to read and understand however once your logic and use-cases grow in complexity the ‘readability’ factor can easily get away from you. This became exactly the case once I started working with Node/Express/MongoDB as a stack for my backend API. Dealing with complex collections and documents within MongoDB and handling their associations, chaining Promises within controller functions often times quickly grew confusing and unreadable.

So I resolved to finally make use of theasync-await keywords and refactor all of my existing code to read more synchronously. Let’s start with that simple API fetch call above and then I’ll go into some greater detail and another use-case.

First off every function has to be declared to be asynchronous right off the bat:

async function getUsers(){
...
}

This declares that zero or more asynchronous await expressions will happen within the context of the function. Next, to make use of error handling we won’t be using .catch() anymore so we instead are able to wrap our function’s inner code in a try/catch block.

async function getUsers(){
try{
...
} catch(error){
console.log(error)
}
}

Next, we can finally refactor the .then() block of our Promise chain by using the await keyword. The biggest thing to understand here is that the await keyword is blocking. It blocks the execution of the rest of the code until the Promise-based functionality that is happening ‘behind the scenes’ is completed. So keeping that in mind, definitely be weary of how many await keywords you use within an asynchronous function. More often then not only when the returned result of an await expression depends on the one before should using multiple awaits be necessary. Usually, if you have many awaits within one async function this can be abstracted into smaller async functions which could potentially run parallel to each other, therefore speeding up your code execution.

Now for the final change in our small code:

async function getUsers(){
try{
const resp = await fetch('https://some-url-here.com/');
const data = await resp.json();
someFunctionToUseReturnedData(data);
} catch(error){
console.log(error)
}
}

We now have the exact same functionality as we did before but written without chaining Promises and in a more succinct, synchronous way that can be easier for people not familiar with your code could understand.

Just to show a quick example of how this can be utilized in a backend Node.js environment as well, I’ll include an example of a Promise chain (req/res) & then a refactored version using the async-await keywords

const Post = require('../models/post')
const express = require('express')
const router = express.Router();
...router.get('/post/:id', (req, res, next) => {
Post.findById(req.params.id)
.then(post => {
if (!post){
const error = new Error('Could not find post');
error.statusCode = 404;
throw error;
} else {
res.status(200).json({
post: post
});
}
});
.catch(error => {
if (!error.statusCode){
error.statusCode = 500;
}
next(error);
})
})

Here we have a Promise chain to fetch a post from a database and to either send back the post in json format or to send back an error describing either that the post couldn’t be found, or that there was a server error. Now lets go ahead and rewrite it using the async-await keywords

router.get('/post/:id', async (req, res, next) => {
try {
const post = await Post.findById(req.params.id)
if (!post){
const err = new Error('Could not find post');
err.statusCode = 404;
throw err;
} else {
res.status(200).json({
post: post
})
}
} catch (error) {
if (!error.statusCode) error.statusCode = 500;
next(error);
}
})

The code behaves the same as it did before but without the Promise chaining and by utilizing async-await it becomes more readable. The simple await expression makes for more synchronous written code.

Refactoring away from Promise chaining helps with readability especially when working on a large and growing application. If you’d like to do more research on async-await, Promises or the Fetch API, below are links to documentation.

JavaScript In Plain English

New JavaScript + Web Development articles every day.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store