Asynchronous Javascript using Promises, Async Await, and Observables with real life examples - Part 1
September 28, 2017
One of the defining features of Javascript is that it’s synchronous and single-threaded so only one code execution process can happen at any one time. For this reason when your app needs to communicate to another server or perform an action which may take a significant time, we want the Javascript to continue running. Asynchronous development patterns help with this and there are many ways to deal with asynchronous events, so this post will list and discuss my favourite 3 which are Promises, Async/Await and Observables .
One of the reasons I decided to write this post was because when I was learning async development a few years ago, it took longer than I’d have liked to learn from all the tutorials and posts out there which showed examples which were too basic. That’s why I’ll try to show some of my own recent usage of real-life code. I’ve also added some examples which can be ran quckly with a simple npm install on my Github repo for Async JS examples.
Intro
Probably the most simple way to show the synchronous nature of Javascript is through the setTimeout function:
console.log("1")
setTimeout(() => {
console.log("2")
}, 1000)
setTimeout(() => {
console.log("3")
}, 1000)
console.log("4")
//Output: 1423
In the example above, the console logs will output the 1 and 4 first, and after around 1 second will run the code in the setTimeout functions, 2 and 3. In reality though, the browser will be doing more complex functions such as sending, receiving or storing data, which is where asynchronous code gets interesting.
When data is being sent/received from the application, we are required to do something when we receive the data (such as manipulating/displaying/saving the data), while the rest of the script can carry on running - this is the crooks of asynchronous programming. A common use case for me is displaying user data on a dashboard, for example:
function getUserName(userid, mycallback){
... //get user data from server using the user id
... //server sends back "response" object with the users data
mycallback(response)
}
const userid = 1;
let userData = getUserName(userid, (response) => {
//Content of this callback will only run when the callback function at the bottom of getUserData is called
console.log(response)
//Output: { username: 'jay' }
})
How the data is received in the above example is not too important. The thing to note here is that we’ve used a callback to execute script after the user data is received using the mycallback function. Callbacks are the fundamental building blocks of asynchronous code in Javascript, and are widely used in the browser and in Node.
The main bulk of this post after the intro will discuss new and fancy ways of writing asynchronous code, but in a lot of cases a simple callback as used here may be perfect to use.
A more complex asynchronous feature of Javascript is AJAX (allowing the browser to request data from an external source without reloading the page). I won’t go into too much detail about this, but the old style XHR API was pretty nasty to use, and as far as I can remember the next step from there was using the jQuery AJAX function which massively simplified data transmission. jQuery’s AJAX function was great because it took away the need to fully understand callbacks as it contained everything in it’s clean API whilst allowing you to chain the functions:
$.ajax({
url: "/getUserData.php",
data: userid,
}).done(function (userData) {
console.log(userData)
})
The .done() or .success() functions are returned from the ajax function of jQuery, again allowing the rest of the script to run and allowing us to respond the the received data once we get it. It makes it easy to read the code and understand what’s going on. jQuery also offered the ability to use promises, which I won’t go into but they are not the same type of promise I’ll be discussing below.
For me, the Ajax feature of jQuery was great, and it was one of the features which got me into Javascript development in the first place coming from a PHP background as it was so simple to get data from the browser to PHP server and vice versa. Now though, with the rise of so many front-end frameworks and paradigms there’s almost a need to work at a bit of a lower level, using native Javascript features to slim down your apps, and this all means different ways of writing asynchronous code.
Promises
Promises are part of the Javascript specification and are used to run asynchronous code. They enable the chaining of functions which pass asynchronous data through objects. In plain English, you can use then()
to access the asynchronous data returned from a Promise, and catch()
to handle errors returned from a Promise, in a similar way to using callbacks mentioned above.
Promises could be used in third party libraries such as jQuery (as mentioned above) or libraries like Q which extend the Promise spec. There are some benefits for using the libraries as they add a few extra nice bits and will, by default, work on many more browsers than the native Promise feature, which is only an ES6 feature so not avaliable in IE for example. That discussion is for another day though! (or this SO post if you want to look now). I prefer to use the native Promise feature set and polyfill backward most of the time.
A simple Promise operation
Here’s a simple promise in action, using a setTimeout()
to simulate asynchronous behaviour to keep it simple. In a real-life situation this will be a server call which I’ll cover later:
let userDataPromise = new Promise((resolve, reject) => {
setTimeout(() => {
let dataExample = [{ username: "jonsnow", dob: "16/08/1900" }]
if (dataExample) {
//resolve: skips to the then() function
resolve(dataExample)
} else {
//reject: skips to the catch() function
reject("There was an error because of the recession or something.")
}
}, 2000)
})
userDataPromise
.then((response) => {
console.log(response)
//Output: [{ username: 'jonsnow', dob: '16/08/1900' }]
})
.catch((error) => {
console.log(error)
//Output: 'There was an error because of the recession or something.'
})
At it’s very basic level the Promise syntax above looks nice and could be compared to callbacks in the way they look, but there are differences. Mainly that a Promise returns a call to a different function based on whether the process was successful using the resolve (success) or reject (failure).
Promise chaining
One other main feature of Promises is being able to chain them together. Meaning that a function which returns a Promise can pass the returned data (resolved or rejected data) to another function (via then()
or catch()
respectively), and keep chaining returns that way. The above example can be extended to include multiple async operations, and chain events to run after the success or failure of each operation.
The below example simulates getting data from a server (server.js) which returns a Promise and chains multiple Promises together:
server.js (back end)
export const getUserData = () => {
... //get userArray from DB query
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(userArray);
}, 1000);
});
};
function getUserData() {
return new Promise((resolve, reject) => {
... //get userData from server (this is asynchronous)
if(userData){
resolve(userData)
}else{
reject('There was an error with the server request because of the recession or something.')
}
});
}
index.js (front end)
function saveUserData(sortedUserData) {
return new Promise((resolve, reject) => {
//save the user data (this would be asynchronous so we will simulate with a setTimeout)
setTimeout(() => {
let saveWasSucesfull = true
if (saveWasSucesfull) {
resolve(sortedUserData)
} else {
reject("failed")
}
}, 2000)
})
}
//start Promise chain
let userDataChain = getUserData()
.then((userData) => {
//at this point, userData has been resolved (received from the server)
//from the getUserData() function
//we can return anything to another then() function, such as manipulated data
userData.push({
username: "gendri",
dob: "12/12/1900",
})
return userData
})
.then((sortedUserData) => {
//we can now access the updated array
//the final then() in the chain will be the resolved value for this chain,
//and we can access this value by another then() function
//from the userDataChain (below)
return saveUserData(sortedUserData)
})
.catch((error) => {
//return the caught error to the final catch handler below
throw error
})
//This will output a Promise pending notice - because at this point the
//userDataChain is still in progress as it contains asynchronous data flow
console.log(userDataChain)
//see output of Promise chain using then(), receiving the final resolved
//values (success or error)
userDataChain
.then((response) => {
console.log(response)
})
.catch((error) => {
console.log(error)
})
Anything returned from a Promise can be passed to a then()
, including another Promise. This is helpful with many situations, including things like the fetch API, which performs a similar set of tasks as above.
As shown above, it’s also important to use throw and catch to pass errors back up the chain to the final output.
Promise concurrency
One of the most powerful and useful features of Promises is being able to run asynchronous processes completely concurrently, and doing something when all processes are finished. This can be done with the Promise.all()
.
Here’s an example I have used in a previous project:
let facebookResponse = ... //Get an array data from Facebook Graph API
let friends = facebookResponse.map(element => {
//new Promise returned for each Facebook friend as we are inside an array map
return new Promise((resolve, reject) => {
//an asynchronous operation for DB lookups
User.findOne({ id: element.id }, (err, user) => {
if (err) reject(err);
var userDetails = {
id: user.id,
name: user.name,
photo: user.photo
};
resolve(userDetails);
});
});
});
//the "friends" array is passed to the Promise.all() function so we can run
//then() when they are all complete
Promise.all(friends)
.then(results => {
res.status(200).send({ success: true, friends: results });
})
.catch(e => {
res.send({ success: false, error: e });
});
One important thing to remember when using Promise.all() is that if one Promise functon fails to resolve, the entire Promise.all() array will fail, so this needs to be factored in.
I was writing some back end code to get data from the Facebook Graph API and loop through them in an array map. Inside the map I wanted to construct my own object format and send that back to the new array (friends). This means though that for each person in the facebookResponse array from the Graph API there will be a new Promise object returned, so we end up with an array of many Promises. That’s where Promise.all()
comes in, which I think is one of the most powerful features of Promises for the day-to-day work I do. In the above example, the newly created array of objects is sent back to my app.
There is a bunch of additional bits to read up on when using Promises, so a few good places to start that is the Mozilla docs or Stack Overflow for some pretty creative usage ideas.
Async await
Async/await is a great part of the ES7 specification which is in most browsers now except IE (of course). It’s built on top of Promises and can be used alongside Promise. One of the main purposes of using async/await is to simplify the code and make it more readable, and it does this by making async code look synchronous.
A simple async/await operation
Here’s a simple example using Promises, and the same result achieved using async/await:
//Promise based
function getUserData() {
return userData()
.then((response) => {
return response
})
.catch((error) => {
alert(error)
})
}
//Async/await based
async function getUserData() {
//notice how the "await" keyword is used before the function which returns the
//Promise as we are waiting for it
let users = await userData()
return users
}
To me, the above code is much easier to read, and this is even more apparent when there’s a lot of chaining of different async functions going on.
This works because the userData()
returns a promise, and we can await
all Promises by adding the “await” keyword, which pauses the execution of the rest of the function until the Promise from the function is resolved. Here’s a snippet from the Mozilla docs:
When an async function is called, it returns a Promise. When the async function returns a value, the Promise will be resolved with the returned value. When the async function throws an exception or some value, the Promise will be rejected with the thrown value.
For clarity, here’s what the userData()
function in the above example could look like:
function userData() {
return fetch("https://mysite.com/api/users", {
method: "get",
})
.then(function (response) {
return response
})
.catch(function (err) {
alert(err)
})
}
The fetch()
API returns a Promise by default and will therefore compliment an Async/await getUserData()
function perfectly, so a common practice is to have this in some sort of API section of your app (separation of concerns et al), and call the API functions elsewhere in your code. The fetch API is relatively new (2 or 3 years) and therefore not avaliable in IE at all, so be sure to include a polyfill or use an alternative solution such as Axios (which also returns Promises!).
Async/await chaining
Promises implements a chaining effect with the then()
and catch()
return functions because of the nature of how it processes asynchronous code, as shown in an example above. Async/await however is designed specifically so there doesn’t need to be long chains of then()
’s:
//use async and await keywords means there's no chaining, and it looks
//like synchronous code
async function userDataFuntion() {
try {
//asynchronous function
let users = await getUserData()
//asynchronous function
let updatedUsers = await _addToData(users)
//asynchronous function
let finalUsers = await _saveUserData(updatedUsers)
return finalUsers
} catch (err) {
console.log(err)
}
}
The _addToData
and _saveUserData
functions are both asynchronous and return a Promise, so we can use the above method to chain the events together. This is very useful in a lot of situations and looks easy on the eyes. Also again, remembering error catching with Async/await is crucial, as this will catch any errors from an asynchronous end point.
Async/await concurrency
Chaining async functions is often needed in situations where you want to pass values from one function to the next, however there’s often a need to have asynchronous functions running in parallel, similar to the Promise concurrency example earlier where each instance of the array map of facebookResponse
runs at near enough the same time.
Here’s an example:
async function dataFuntion() {
try {
let dragons = getDragonData();
let houses = getHouseData();
//await using Promise.all
let theData = await Promise.all([dragons, houses]);
} catch (err) {
console.error(err);
}
As mentioned earlier, async/await and Promises are not mutually exclusive, and in fact are often great when used together. The Promise.all()
can also be used in the situation to resolve when all functions have completed, and by awaiting the entire set of Promises in one go, the code can still be asynchronous and not have a load of then()
and catch()
chains.
Again, remember that the Promise.all() will fail if one Promise inside the array fails.
Fin
Promises are great and kind of exciting to use, especially if you’ve previously been using callbakcs for years, and its even better when they are used alongside the async/await syntax to keep everything looking synchronous.
In part 2 of this post, I’ll be diving into Observables, which are different to Promises and arguably more complicated.
Hit me up if anyone has questions!
Senior Engineer at Haven