Understanding Asynchronous, Callback(Hell) in Javascript

frontend7 Min to Read08 Aug 17

If you are a javascript developer, you will see lots of asynchronous code. Do you know what is asynchronous? Have you wonder why we use asynchronous code and why can't we just use synchronous code? Well! that is what what we are going to learn in this article.

To understand what is asynchronous, we will start with a hypothetical example. See below three lines of pseudo code. Just imagine that you can run this code in any programming language then try to answer the question asked below.

Before giving answer, one more asummption: func1() in below code returns 6 somehow. By 'somehow ', we mean it might fetch '6' from network or database or server or just local variable.

what will be the output of line3?

1. var1 = 8;
2. func1();
3. print var1;

func1(){
  var1 = fetch6somehow();
}

Answer: You might say 6; you might say 8; Well! it depends on the host programming language that is used to run above code and how func1() is fetching 6. See below section for explanation.

If host programming language decided to wait for the result of fun1(), even if it takes very long time, then results will be 6. If host programming language decided to not wait for the result of fun1(), even if it takes very less time, then the result will be 8. How does the host language decide this?

Well! they decide based on what operation we are doing inside func1() and what API we are using to perform this operation. For example1: if func1 is fetching number '6' from server through network, then this function might really take a long time, in such situation, host language might decide not to wait for func1 because if it waits for func1() then, it will block other codes('after line2') from running for long time. Such non-blocking way to run func1 is called asynchronous way.

For example2: if func1 is returning number 6 from local variable then, func1 will not take much time to finish its work, in such situation, host language might decide to wait for func1 before executing a subsequent line of code. Such blocking way to run func1 is called synchronous way.

Please note, if the function is doing any operation that takes long/indefinite time then it does not necessarily mean that it has to be done in an asynchronous(non-blocking) way. Many languages provide the API to do the same task in both ways(async & sync). We will see example in a subsequent section.

why async?

why do we need async at all? why can't we just do it in synchronous way? See the reason below.

Reason1

Just imagine more lines of code in above sample code and more operation/function like func1() which is blocking(synchronous) in-between the code. This might not be the problem for small line of code, neither its a problem for a Multithreaded language where we can split/distribute such blocking code across multiple threads.

But think of Javascript, it's single threaded; if we have so many blocking synchronous code on the main thread, how user's experience will deteriorate. So we need a way to run our code in a non-blocking way(async way), giving a chance to other code to run parallelly.

Reason2

There are some works/operations which are not blocking but yet inherently asynchronous. We are not sure of when the result will come of such operation, their results are very indeterministic. For example, event based operation/work. We know that Javascript is event-oriented language, so we write lots of event based logic in our program. Such event-driven programming in Javascript is asynchronous in nature.

Summary: Asynchronous operation is not just about operation. It's more about how we are doing that operation. Are we doing it in blocking way? yes! then it's synchronous way. Are we doing it in a non-blocking way? yes! then it's asynchronous way. It simply means: we can perform the same operation in a synchronous and asynchronous way(both way). You will see some example below that some language provide both ways to perform the same operation. Generally, long time taking operation is done in async way and for that, language provide some API to do that.

Example

In Javascript, Reading from and Writing to localStorage using api localStorage.getItem() and localStorage.setItem() are synchronous. Reading from and Writing to indexdDB is asynchronous. Making an ajax call using api XMLHttpRequest is asynchronous.

Using event based logic i.e onLoad, onKeyDown, onScroll etc inside any function, makes it asynchronous function.

In Node.js, for file read operation in synchronous way, we use fs.readSync() api and for same file read operation in asycnchronous way, we use fs.read() api.

Problem wih async

Async programming does not change the original problem, it does not even change the original solution. Instead, it creates a different set of problem, for which we require a different solution. The problem it creates is the way we think that our code will run line-by-line and one-by-one, which is incorrect in async programming. So thinking and writing code in this way, might result in unexpected result like we saw the pseudo code example in introduction section.

Lets take another example as shown in below code. Requirement is simple, we want to print the result returned by function getRandomText().

function getRandomText() {
  setTimeout(() => {
    var text = "Hello Reader"
    return text
  }, 1000)
}

function printRandomText(result) {
  console.log(result)
}

//see here, 2 lines of code
var result = getRandomText()
printRandomText(result) //print 'undefined'

In above code, if you think that function printRandomText() will print 'Hello Reader' then you are wrong. It will actually print 'undefined' because javascript engine will not wait for function getRandomText() to return its result 'Hello Reader' because getRandomText is an asynchronous function. Sow now question is: what is the solution that function printRandomText print expected value 'Hello Reader' instead of printing unexpected value 'undefined'.

Solution: Callback

The solution is simple: change the way we think and write; change in writing might require a different coding pattern or a different syntax; For example, use some API that browser provides for async task like Promise; Use different coding pattern like Callback for an async task. In this article,we will learn about callback as a sultion to this async problem.

function getRandomText(fnPrintText) {
  setTimeout(() => {
    var text = "randomText"
    fnPrintText(text) ////print expected result
  }, 1000)
}

function printRandomText(result) {
  console.log(result)
}
//
getRandomText(printRandomText)

See below code for the solution of above problem. See how we have modified our function getRandomText. Now we are also not calling function printRandomText directly, instead, we are passing the reference of it as an argument to function getRandomText(). Such way of passing one function as a parameter to another function is called callback. Doing this way will make sure that function getRandomText call function printRandomText at an appropriate(right) time.

Summary: So solution to asyc programming is callback. Callback is a way to pass one function(callee) as an argument to another function(caller). Callback ensure that caller call callee at right time.

Callback Hell & How to Avoid

Callback Hell is a situation, resulting from bad coding pattern/practice. We come into this situation, when we use too much callback in a nested fashion. See below code for example.

When do we come into this situation? when we have requirement like below code. Requirement is simple, each function is dependent on the result of its previous function and ofcourse each function is asynchronous.

//Suppose below pseudo code is requirement
//See how each function is dependent on its previous function
text1 = funcGetText1()
text2 = funcGetText2(text1)
text3 = funcGetText3(text2)
printFinalText(text3)

Why do we come into this situation? because we are using too much callback in a nested fashion. See below 'CallbackHell: Bad' example, just imagine if there are more nesting. It will be very difficult to maintain and hard to read such code. In such situation, it is very easy to make an error in the code.

//Below is callback hell, just imagine if there is more nesting
funcGetText1(function(text1) {
  funcGetText2(text1, function(text2) {
    funcGetText3(text2, function(text3) {
      printFinalText(text3)
    })
  })
})
//This is much better, easy to read and understand
var fnCallback1 = function(text1) {
  funcGetText2(text1, fnCallback2)
}

var fnCallback2 = function(text2) {
  funcGetText3(text2, fnCallbackFinal)
}

var fnCallbackFinal = function(text3) {
  printFinalText(text3)
}

funcGetText1(fnCallback1)

How to avoid such situation: we can't avoid it completely but we can reduce its severity by using different coding pattern. We still use callback but in a different way, see the above example 'CallbackHell: Better'.

In Javascript, there are better alternatives to callback for async programming. For example, promise and async-await. Promise is matured API, but async-await is relatively new. To know more about it, check this article on Promise and Async-Await.

If you loved this post, Please share it on social media.