Have you ever faced the situation where you write some Javascript Code, and find out that it's not executing the way you want it to be? Intuitively we assume that the code will run in the same order as we specified, but in reality, it just runs on its own. This happens due to callbacks, and falling into this unfortunate situation is called the callback hell. Unless you know how to work through synchronous and asynchronous calls in Javascript, there is no way of getting out of this hell.
Understanding the concepts like sync, async, yield, promises etc, is important, but that alone will not help you write Javascript code to work the way you want it to be. This blog is intended for beginners who have no idea on how to handle the situation of callback hell, and a general background knowledge on JavaScript is needed to go further.
What is a Callback Hell?
The code in the above image looks well in order right? But this deception is what doesn't make you realize that you are falling into a callback hell. Whenever the sign " } ) " comes repeatedly in a code, this is where callbacks are going deep into other callbacks and making you fall deep into the callback hell.
The callback hell occurs, when people code and expect the code to execute line by line, one after the other. This common misconception is what leads to your code going haywire, whereas this does not happen in languages like Java, Python, C etc, unless explicitly specified. Before we go further, let's understand a few of the important concepts in JavaScript callbacks.
A callback hell being good or bad is dependent on the coder and the type of execution that is needed. Some people prefer to have the callback hell, since they are used to coding like that, and they are not worried about the readability of the code. But in general it is a good practice to avoid it, as it can lead into unnecessary problems, and debugging it would make your life even harder as it goes deep into the hell.
Anonymous Functions
These are functions which are created dynamically during run-time, and these do not require a function name (You can provide a name, if you wish to use it recursively). The following image shows the difference between a named function and an anonymous function.
There are two ways to define anonymous functions in JavaScript.
These anonymous functions are generally used as callbacks. The anonymous function is executed right after an event has occurred. Now let's have a look at callback functions in JavaScript.
Callback Functions
A callback function (also known as a higher order function) is a function that is passed into another function, which will execute the callback after a certain event has occurred.
Something important to note about callback functions, is that they are not executed immediately. The name "Call Back" implies that it is a function that will be called back later, within the function body. The issue that everyone faces with callbacks is the order of execution. With proper coding practice and understanding, you can easily avoid falling into the callback hell and take control of your code.
These are generally used with async functions like I/O operations, where we need the callback function to be executed, soon after the async operation is over. Due to this reason, the time that a callback can occur is undefined, and it depends on the time taken to execute the async call.
How to avoid the Callback Hell
The problem with callback hell, is the hard readability and maintainability. Even though it gives flexibility to the user, it takes away the control from the user unless proper knowledge is used when writing the code. Therefore, it is always important to avoid callback hell and have proper control of your own code. Given below are a few ways in which callback hell can be avoided, with simple examples to understand them easily. Most of the time, you might want to use some of the below approaches together, in order to properly avoid writing bad code so that it works as you want it to work.
1) Modularization of Code
It is always a good practice to modularize your code, so that it improves readability and reduces redundant codes in the code-base. This can either happen in file level where separate files represent separate functions like in NodeJS (where you export the module), or as separate functions within the same file. The latter is about breaking your code into different functions so that it orders your code nicely.
The code given above, is very messy and could easily take us into a callback hell, where proper modularization can make it easier for us to avoid it. In NodeJS, you can use separate modules, but given below is an example for breaking the above boilerplate code into a neat code segment.
2) Using Promises
Using promises can, not only improve your code's readability, but also give you the power to order your code to work synchronously from top to bottom. A promise is an object that wraps around a certain process that needs to executed, and it promises that it will happen after the event we specify. I will not try to explain what promises are. You can get a thorough idea from this link.
If you don't really care about the order of events, you can consider using " Promise.all ( [ ... ] ); " which will resolve all promises at once. For example, if you want to retrieve many images all at once, you can use this where the order is not really important.
3) Proper Error Handling
Whenever you are using async functions, there is always a possibility of getting errors like "invalid file", "permission issues", "network issue" etc. If this happens in the middle of your callback hell, it makes the life of the programmer feel like hell, because he would not have any clue as to what caused the issue. Therefore it is always a good practice to handle errors in async functions, and work them through with callbacks.
When using callbacks, the first argument is always reserved for errors, so it is easy for the programmer to handle the error, if the specific event causes and error. The below image depicts the convention used in callbacks.
4) Use of Generators
Generators are similar to iterators, but it allows us to specify our own algorithm by writing a single function that can be used to maintain the state of an object. This also brings in a sense of order into the code, which can increase the readability of the code.
Generators are defined by adding an asterisk (*) after the function operator. An example is given below. You can see that we can maintain our own order which will make it easier for the coder to manage the state of a program or object.
If you look at the above code, you can see how the three DB calls are being synchronized as needed.
References