Basics of Callbacks and Promises in Javascript

Patricia Nicole Opetina - Jun 14 '21 - - Dev Community

[JS#3 WIL πŸ€” Post]

Most websites and applications written would, at some point request data from a server, wait for user input or in general do other processes that would take a good amount of time to finish. Because of this, Javascript supports asynchronous functions, simply, functions that can run in the background while other parts of the program execute. These functions are executed in its entirety when called, but might finish on some future time.

One, and the simplest way of achieving asynchrony is by using callbacks.

πŸ“Œ Callbacks

A callback is a function passed as an argument to a different function. They are executed asynchronously or at a later time. Practically, programs are read from top to bottom, but this is not always the case as async code may run different functions at different times. For instance, when handling click events for buttons in an HTML form, we typically do this:

submitBtn.addEventListener("click", 
  //this function is a callback
  function() {
    printSomething();
  }
);

function printSomething() {
   console.log("Hello, Dev Community!");
}
Enter fullscreen mode Exit fullscreen mode

In the snippet above, the addEventListener function takes a callback, in this case the function invoking the printSomething method. It will then be invoked when the submitBtn is clicked.

Using callbacks are pretty easy and convenient when dealing with simple processes. However, it may quickly get out of hand if they are chained or nested deeply together, famously known as Callback Hell or pyramid of doom 😟.

So, callbacks are very fundamental in JS asynchrony. However, as the language grows and with program logic getting a little too complex, they are not enough. The future of JS requires a more sophisticated way of async patterns, one such mechanism is a promise

πŸ“Œ Promises

A promise is an object that MIGHT produce a value at some future time. For example, let us say that the function fetchDatabaseData gets data from a server and returns it as an object.

function fetchDatabaseData() {
  //fetches data via some API
  ...
  return api.getData();
}
Enter fullscreen mode Exit fullscreen mode

Fetching the data from the server may take time, so unless we tell the code that, it will always assume that the data is ready. The api.getData() is probably not yet finished, so, executing the below snippet may have an unwanted result, e.g. undefined 😣.

const apiData = fetchDatabaseData();
const firstEntry = apiData['first];
Enter fullscreen mode Exit fullscreen mode

So, to fix this problem, we need a mechanism to tell our program to wait for fetchDatabaseData to finish. Promises solve this issue for us.

To fix the above code, we can do something like,

function fetchDatabaseData() {
  //fetches data via some API
  ...
  return new Promise(function(resolve, reject) {
    const result = api.getData();
    if(result == undefined) {
       reject("Ooops, we've got an error");
    } else {
       resolve(result);
    }
  });
}

const apiData = fetchDatabaseData();

apiData.then(
  // handle fulfilled operation
  function(data) {
     const firstEntry = apiData['first']; 
     console.log(firstEntry); 
  },
  //handle rejection error
  function(err) {
    console.error(err);
  }
);
Enter fullscreen mode Exit fullscreen mode

From the example above, a promise behaves as a 'future' value. Since the time-dependent process is encapsulated inside the promise, the promise itself can be treated as time-independent. Meaning, it can be combined with any other promise regardless of how long the encapsulated process might take, without any problem.

When the result from the api is undefined, the returned Promise will have a rejected state. Thus, the then method would print "Oops, we've got an error".

On the other hand, if the api results to a defined object, the Promise would be fulfilled and the then method would print the api results.

In addition, immutability is one of the most essential aspect of promises. Once a promise has been resolved, it stays that way FOREVER. It becomes immutable. You can then pass the promise around and know that it cannot be modified maliciously or accidentally.

I created a very simple weather application using the fetch function which returns a promise. The live preview and the code can be found in github.

I also recommend reading these chapters of You Don't Know JS : Callbacks in Chapter 2 and Promises in Chapter 3
for a more in-depth discussion of these concepts.

In conclusion, there are a lot of ways to handle asynchrony in Javascript, two of them via callbacks or promise. Callbacks can be used in handling simple logic but might get out of hand if chained, i.e. callback hell. Promises do not get rid of callbacks, they try to address callback chains by expressing the asynchronous program flow in a sequential way. It helps in writing maintainable JS code. It also makes the code more readable as the program-flow is sequential, which is similar on how our brain plans and executes actions.

Cheers to continuous learning! 🍷

[REFERENCES]
[1] Art of Node
[2] The Odin Project: Async
[3] You Don't Know JS

. . . . . . . . . . . . . . . . . .