The Curious Case Of Callbacks Vs ES6 Promises

Introduction

 
Hi everyone! Feels nice to be back with one more write-up on C# Corner. 
 
The title over here has totally summed up my views regarding the two most fundamental concepts in the world of JavaScript prior to writing this piece.
 
Got to know from a lot of sources over the web about the good things before bringing these two concepts to the table. But most of them ended up proving to me that both of them do the same thing under the hood. After investing some more time to eke out any differences, I somehow got a near to convincing answer.
 
Let’s try to throw some light on the same here.
 

Callbacks

 
Callbacks are plain JavaScript functions that execute after completion of an asynchronous/non-blocking operation. Since these asynchronous operations (example Database interactions/API calls) usually take some time to finish, in most of the cases, callbacks provide the means to notify the end-user upon their completion. A general way of making use of callbacks is provided below.
  1. someAsyncOperation(args, callbackFn() {    
  2.     // some code here    
  3. });   
Here, ‘callbackFn’ is the callback function that gets into the action as soon as ‘someAsyncOperation’ completes, and does some kind of notifying or follow-up work.
 
Example
 
Notifying the user that the operation is complete.
 
Promises
 
The ES6 update to JavaScript brought with it a ton of cool new features and abstractions over some redundant pieces of functionality which required all those truckloads of lines of code to be written in Vanilla JavaScript.
 
One of them was the introduction to the concept called Promises. Promises are similar to callbacks in a way that they look after an asynchronous operation’s execution flow. Given below is a general representation of how promises operate.
  1. var somePromise = new Promise((resolve, reject) => {    
  2.     //some async operation    
  3.     resolve(response) // if the async request completes successfully    
  4.     reject(error) // if some error occurs while processing the asynchronous request    
  5. });    
  6. somePromise.then((response) => {    
  7.     // some code to execute if the promise is resolved    
  8. }).catch((error) => {    
  9.     // some code to execute if the promise is rejected    
  10. });   
A promise, in other words, is simply a promise which gets returned either in a resolved or rejected state. You just wrap the supposed asynchronous operation inside a promise object and what the promise invoking code gets is an assurance that the promise will take care of the operation and return either a ‘resolved’(successful) or a ‘rejected’(erroneous) response.
 
And then, you have the then!! .then() is a promise handler which takes the response out of the promise object and works on it in some manner just as the callback does.
So what do we infer?? A promise is just another way of implementing a callback. Is it or it isn’t?
 
Hmm, so to clarify on those doubts you would still have, let’s go one step further and look at this conclusive use case.
  1. //callbacks.js code    
  2. const asyncAdd = (a, b, callback) => {    
  3.     setTimeout(() => {    
  4.         if (typeof(a) != “number” || typeof(b) != “number”) {    
  5.             callback(undefined, “Must provide a valid input”);    
  6.         } else {    
  7.             callback(a + b);    
  8.         }    
  9.     }, 2000);    
  10. }    
  11. const asyncAddOne = (x, callback) => {    
  12.     setTimeout(() => {    
  13.         callback(x + 1);    
  14.     }, 1000);    
  15. }    
  16. const asyncSquare = (x, callback) => {    
  17.     setTimeout(() => {    
  18.         if (typeof(x) != “number”) {    
  19.             callback(undefined, “Cannot compute square of a non - numeric input”);    
  20.         } else {    
  21.             callback(x * x);    
  22.         }    
  23.     }, 1000);    
  24. }   
If you see, the functions defined above in callbacks.js are normal arithmetic functions that have been realigned using the setTimeout method to simulate asynchronous behavior. (For those, who don’t have any clue about setTimeout, it is a function in JavaScript which triggers any method wrapped inside it after a specified time interval). Do make a note that we are doing a type checking on the operands passed in these functions to make sure that only numbers are provided for these arithmetic operations to complete. In case an invalid input is provided like non-integer values, the function will send an error back in its response.
 
Now, let’s put some code in our app.js file which shall make use of callbacks to process the response emitted by these asynchronous methods in callbacks.js.
  1. //app.js code    
  2. //using callbacks    
  3. console.log(“Using callbacks”);    
  4. callbacks.asyncAdd(2, 4, (resAdd, error) => {    
  5.     if (error) {    
  6.         console.log(error);    
  7.     } else {    
  8.         console.log(`The result of asyncAdd is ${resAdd}`);    
  9.         callbacks.asyncAddOne(resAdd, (resAddOne) => {    
  10.             console.log(`The result of asyncAddOne is ${resAddOne}`);    
  11.         });    
  12.     }    
  13. });   
In real-world JavaScript applications, you’ll come across situations where you might need to use the response of an asynchronous operation (Database/API calls) as an input for some other asynchronous operation waiting in a chain. It’s here that callbacks make a case for themselves. (For example, fetching latitude /longitude details from a geolocation API and, in turn, providing these details as input to a weather API).
 
We’ll get some glimpse of this happening in our app.js file which has a couple of functions namely ‘asyncAdd’ and ‘asyncAddOne’ chained together. The response we get from the ‘asyncAdd’ function is provided in the callback as an input for the ‘asyncAddOne’ method which simply adds 1 to the input and returns back the computed value. Also, in case the arguments provided at the first place were invalid, there is a check in place inside the callback function which simply logs it to the console. This seems to be a pretty trivial use case.
 
Take some time to imagine 10–20’s of asynchronous methods chained in procession in app.js after the ‘asyncAddOne’ function. Hard to imagine! Well, 10–20 becomes too small a number sometimes in the world of JavaScript and asynchronous programming. But yes, if we keep using callbacks, we’ll definitely find ourselves in a deeply nested code with every asynchronous call wrapped inside a callback and I am scared to tell as to how readable your code will remain for others to see. Add to that, the need to check errors in the response returned by every asynchronous method and all hell will break loose. Ah indeed, no wonder this phenomenon is referred to as the callback hell in the JavaScript world.
 
Alright, any healing touch that promises might provide here? Without much ado, let’s jump into the same use case this time making use of promises. For the sake of better separation and code readability, I‘ll move the asynchronous functions (previously in callbacks.js) to a new file called promises.js.
  1. //promises.js    
  2. const asyncAdd = (a, b) => {    
  3.     return new Promise((resolve, reject) => {    
  4.         setTimeout(() => {    
  5.             if (typeof(a) != “number” || typeof(b) != “number”) {    
  6.                 //throw new Error(“not a valid input”);    
  7.                 reject(“not a valid input”);    
  8.             } else {    
  9.                 resolve(a + b);    
  10.             }    
  11.         }, 2000);    
  12.     });    
  13. }    
  14. const asyncAddOne = (x) => {    
  15.     return new Promise((resolve, reject) => {    
  16.         setTimeout(() => {    
  17.             resolve(x + 1);    
  18.         }, 1000);    
  19.     });    
  20. }    
  21. const asyncSquare = (x) => {    
  22.     return new Promise((resolve, reject) => {    
  23.         setTimeout(() => {    
  24.             if (typeof(x) != “number”) {    
  25.                 reject(“Cannot compute square of a non - numeric input”);    
  26.             } else {    
  27.                 resolve(x * x);    
  28.             }    
  29.         }, 1000);    
  30.     });    
  31. }    
  32. module.exports = {    
  33.     asyncAdd,    
  34.     asyncAddOne,    
  35.     asyncSquare    
  36. }   
So, pretty much the same functions you’ll see here apart from one extra asynchronous function (asyncSquare) which has been put here to better explain how promises deal with the issue of chained asynchronous calls compared to callbacks. As mentioned in the introduction to promises earlier, we make use of resolve and reject to exhibit the status of the asynchronous call back to the caller. If the computation is successful, we send the response wrapped inside a resolved statement else, we send the error in a rejecting statement. Now, let’s have a look at how the promise gets handled as its returned back to the caller method.
  1. //app.js code    
  2. // using promises    
  3. console.log(“Using ES6 Promises”);    
  4. promises.asyncAdd(2, 4).then((resAdd) => {    
  5.     console.log(`The result of asyncAdd is ${resAdd}`);    
  6.     return promises.asyncAddOne(resAdd);    
  7. }).then((resAddOne) => {    
  8.     console.log(`The result of asyncAddOne is ${resAddOne}`);    
  9.     return promises.asyncSquare(4);    
  10. }).then((resSquare) => {    
  11.     console.log(`The result of asyncSquare is ${resSquare}`);    
  12. }).catch((error) => {    
  13.     console.log(error);    
  14. });   
Hmm, where are those nested lines of code? Ohh, there are none! Exactly!
 
The .then() handler here is the key.
 
Note

When the promise is returned back to the handler, we refer to the promise to be in a settled state. While the async operation is in the process, the promise is said to be in a pending state.
 
So, the then() handler takes the settled promise returned from the first asynchronous call (asyncAdd), checks if the promise is resolved or rejected and based on it, triggers the next asynchronous call or simply propagates the error to the .catch() handler which emits the error and suspends any further asynchronous calls.
 
Any visible benefits of using promises?- Certainly!
 
The code looks much readable now with any developer in your team not having to scratch his head to get to know what exactly is happening with the code. Also, only a single catch handler to catch any error thrown at any stage of the chained async operations in place of multiple ‘if’ statements in case of callbacks!
 
Any questions popping?? — which use cases advocate the usage of promises over callbacks?
 
Although the bottom line is that both, callbacks and promises, do the same stuff for you, promises will surely come across as a better fit for cases that otherwise demand a deeply nested tree of asynchronous calls with callbacks.
 
But the choice is totally yours!!
 
Do keep in mind that promises are simply wrappers around callbacks which are fine-tuned for performances in cases of long nested asynchronous calls.
 
But yes, when the chain is relatively small (the definition of small??…will keep it open-ended), even callbacks should be good enough to do the job for you.
 

Summary

 
I’ll park those thoughts for now and look forward to some interesting ones coming from your side.
 
Till next time!…


Similar Articles