Understand how asynchronous work is done in JavaScript, callback based, promises and async/await.
First of all, lets understand the idea of asynchronous code. Taking a human body as a typical idea, we have many organs and systems that can connect and communicate with each other, our body systems all perform a specific task or group of tasks, now imagine that you can't hear while you are seeing? i mean if your listening to someone that your brain would not be able to act to data coming in from the eye? Or you can't think while walking? Basically waiting for one thing to finish before we go to next one. Our lives would be terribly slow! However we don't work like that and thank God for that, our systems all function at the same time, we can hear, see, feel, talk all at the same time without a hassle, When the signals from the eye comes, the brain fires a response to that and if signals for hearing is also available it reacts to that.
The idea behind asynchronous operations is that our code should not be stuck wait for one thing to finish before moving to the next one, we can start one task now, move on the next one and then come back to the one we started and complete it later! This comes in handy when we want to something that will take some time, so our code doesn't freeze and spoil the UX for the user. There are different means that JavaScript uses to achieve asynchronous operations, although JavaScript itself is single threaded this implies that code written in JavaScript only runs one task at a time, JavaScript asynchronous operation through the following means:
- However JavaScript is also a functional language and this means that we can pass functions as arguments to functions and if the function we passed in depends on a value in the main function it will wait for it.
- We can also use promises to deal with them and they have a cleaner syntax than callback based code.
- Async/Await and this is the easiest way to manage asynchronous code
//Normal Synchronous code
let hero = 'spiderman'
console.log(hero)
let hero2 = 'Antman'
console.log(hero2)
The following code would log out spiderman before antman proving that javaScript is single threaded however the browser provides a useful API, the setTimeout() method, this adds a function to the queue after a given time has elapsed, The setTimeout function takes two parameters as arguments, an function and an integer which is a representation of the number of time we want to elapse in milliseconds before we call the fuction we pass to it as argument.
console.log('starting')
setTimeout(()=> console.log('timeout ran'), 300)
console.log('before timeout')
And we see that before timeout is logged to the console and then timeout ran follows, if we leave the argument for time empty it will still behave the same way, basically this API tells the browser to add our code to the call stack after some time, which maybe the time taken get a resources or do some work and this forms the basis of callback code, let's look at callback based code.
Callbaclk Based Code
Callback based code is usually the first solution to asynchronous programming and it involves passing a function as argument to another function, the function we passed as argument will delay execution until the initial function has finished running, then the function we passed as a callback will then run, lets look at a typical example;
console.log('starting')
let fun = (cb) => {
let myHero = 'hulk'
let hero = 'Cyborg'
let heroI = 'Superman'
setTimeout(()=> cb([myHero, hero, heroI]))
}
fun((hulk)=> {
myHeroes = hulk
console.log(myHeroes)
}
)
let myHeroes;
console.log('before timeout')
//logs out
// starting
// before timeout
// ['hulk', 'Cyborg', 'Superman']
Clearly we see that 'before timeout' is logged out to the console before the fun function logs out myHeroes even if we are calling the fun function before we log out 'before timeout' to the console, This is JavaScript telling our code to go on to the next task and when we have a result from fun, log that to the console. This is a typical example of making our code asynchronous, Let's see a typical use case of callbacks with http request using the XMLHttpRequest object.
This is an API that is available in the browser and it allows us to make http request without breaking the UX, it behaves asynchronously meaning it can start and then finish at some point. We will write a simple reusable function that allows us to get data from some resource and do some thing to it
let request = function(url, cb){
let XHR = new XMLHttpRequest();
XHR.open('GET', url, true)
XHR.send(null)
XHR.onload = function(){
if(this.status === 200){
cb(undefined, XHR.response)
}
else if(XHR.status !== 200){
let err = { message: 'Error fetching resource', status: XHR.status}
cb(err, undefined)
}
}
}
Our request function will called with two arguments, the url of the resource we want to fetch and a callback function, the callback function has access to two parameters, an error object if there is one and a data that represents the resource we were trying to get if it was found, Let's call this function and try to get some data from json todos placeholder
console.log('before request')
console.log(1)
request('jsonplaceholder', (err, data) => {
if(!err){
console.log('request completed', data)
}
else{
console.log('request completed', err)
}
)
console.log('request made')
console.log(3)
We should see the resource logged out to the console if it was successfully gotten or we see an error object logged to the console. This is cool because it is reusable and it abstracts away some code, however this can easily turn out into a triangle of doom, if we had to get some more resource when we have gotten the first resource, our code can easily get messy
request('jsonplaceholder', (err, data) => {
console.log('request completed', data)
request('jsonplaceholder', (err, data) => {
console.log('requset completed', data)
request('jsonplaceholder', (err, data) => {
console.log(data)
})
})
)
Our code just gets this depth eating into it and if there is an error how do we know where the error is??? Let's say we create a separate error handler function and make it reusable, however we will still have the triangle of death eating into it, rather than use callbacks you can use promises.
Promises
Promises represent a cleaner way of performing asynchronous tasks, a promise basically will return the result of an asynchronous process and you can access that using a then method to handle the data, or a catch method to handle errors, let's see the basic syntax of a promise
console.log('before myProm called')
let myProm = new Promise((resolve, reject) => {
if(1 < 2) resolve(true)
})
console.log('myProm defined')
myProm.then(data => console.log('got data back', data))
console.log('after myProm called')
//logs out
//before myProm called
//myProm defined
//after myProm called
//got data back true
We see that the code in the then method is fired last proving that promises are asynchronous. A promise is declared using the Promise constructor, it takes a function as an argument and that function we pass as argument to the promise takes in two parameters, resolve and reject. We use call resolve to return a value from the promise if everything is okay, we call reject to return an error if something is wrong. The data that is resolved can be accessed using the then method, it takes in an argument argument represents the data that is resolved by the promise and in the example above we just log it to the console. We didn't handle failures in our above example, but if there was a failure we use the reject parameter and reject a value with it, the data that is returned by the reject method is made available on the catch method, and we can use that for error handling. Let's see a typical case of a promise failing.
console.log('before myProm called')
let myProm = new Promise((resolve, reject) => {
let myVar = 10;
if (1 >= myVar){
resolve(true)
}
else{
reject(false)
}
})
console.log('myProm defined')
myProm.then(data => console.log('got data back', data))
.catch(err => console.log('oops something happened', err))
console.log('after myProm called')
//logs out
//before myProm called
//myProm defined
//after myProm called
//oops something happened false
The promise in the above example is rejected because clearly, 1 is not greater than or equal to 10, so we call reject and pass it false as an argument and when we handle this error we see the false statement, we could also pass in objects as values to the reject and resolve method, let's modify our XHR function to use promises instead of callbacks
let request = function(url){
return new Promise((resolve, reject) => {
let XHR = new XMLHttpRequest();
XHR.open('GET', url, true)
XHR.send(null)
XHR.onload = function(){
if(this.status === 200){
resolve(this.responseText) //new addition
}
else if(XHR.status !== 200){
let err = new Error('Error fetching resource')
err.status = XHR.status
reject(err) //new addition
}
}
})
}
//requesting our data
request('data.json')
.then(data => console.log(data))
.catch(err => console.log(err))
//logs out the data
I believe that you would agree with me that the above example is a much cleaner and easier way of writing asynchronous tasks, cool and neat and if we want to make multiple requests that are dependent on an earlier requests we won't have that triangle of depth eating into our code, and we don't have to worry about call back hell, let's see a typical use case
request('data.json')
.then(data => {
request('data.json')
.then(data => console.log(data))
})
.catch(err => console.log(err))
We see that our code is still looking cleaner and we still understand what is going on, we only have to call the catch method once and it handles any promise rejections in the code even if we nest promise call after promise call, I think this is easier to work with than callbacks. Promises are cool although they still have their drawbacks and with more request being made our code could easily start looking messy, thank God for we have async/await.
Async/Await
Async/Await are a new feature of JavaScript and that make handling asynchronous easy, we can mark a function to be asynchronous using the async
keyword and then we use the await
keyword to await some asynchronous task and continue writing other logic inside our function. async/await is a much improved way of dealing with promises, let's see how we can use the async/await with an asynchronous task, we are still using the request function we declared using a promise;
let getResource = async () =>{
let response = await request('data.json')
console.log(response)
}
getResource()
You will look at these and wonder why i didn't go straight into async/await? async/await makes asynchronous tasks a dream as demonstrated above, when we use the async keyword to mark a function as asynchronous meaning at some point we will do some form of asynchronous work and then inside the function we use the await keyword before the asynchronous operation and in our case it is the request function, now this is what will happen; the function will start executing and when it encounters the await keyword it will suspend the function and move on to the next thing, when the value from the request is available it continues with the async function and we see our data logged out to the console. The await keyword simply waits for a promise to evaluate and return a value or an error and then we continue with the rest of the code. That is it for this article, hope you find it useful, have a nice day.