Callbacks - A thing of the past

If you are a web developer you probably spend a considerable amount of time handling requests, waiting for things from external sources and then handling the requested data. The problem is quite common and not easily manageable on the client-side. A new age is upon us and it is pretty exciting. Handling client-side requests aren’t going to be the same anymore.

Callbacks of the past

To be able to understand the changes in JavaScript requests handling and their significance we are going to have a look at the past and see how things were done back then.

Imagine that you’re back in 2014 and you need to request data from a server. The server sends your data at a slower rate than the rate at which you render the changes and there’s an undefined variable. You realize that you’ll need to wait for the data, so you decide to implement a callback function. The callback function will be passed as a definition to another function and when that function gets the needed data it will proceed on to execute the callback function that you sent. 

This ensured that all functions are called in the right order. In short, that is how requests were handled back then.

A problem

Let us see why the need for a new solution has arisen, or to be more specific, let’s see where the problem lies. 

const serverDocuments = [...]
function getDocuments(){
  setTimeout(()=> {
    for (const document of serverDocuments) {
      console.log(document.name);
    }
  }, 200);
}
function addDocumentOnServer(document){
  setTimeout(()=> {
    serverDocuments.push(document);
  }, 1000);
}
addDocumentOnServer({id:9, name:"New Document", size:0.92});
getDocuments();

 

As you can see, we have two functions. One gets all documents from ‘the server’ and the other adds a new document to ‘the server’. We simulate a server by having an array with documents and timeouts that mimic a server response time. So, if we try to add a new document and then ask for all documents, in theory, we should get all the documents that were previously available plus the one that was added. 

But we only get the documents that were initially available, without the new document that was supposed to be added. This is because the adding of the document took longer than getting all documents from the server. 

In order for the JavaScript code to run without being blocked, it delegates code out its single stack and then it comes back to the stack only when that particular code is ready (timeout, http call, etc.) This is the reason why the Add function is executed second, even though we wrote it first. Since it gets thrown out of the stack, it returns AFTER the get function. Developers realized that they had to do something and try to solve the issue, so they started using callback functions.

 

Callbacks solution

Callbacks are, basically, functions that are passed as a value to other functions. They were used for handling problems like the one described above for years. The solution is simple. We make a chain of functions ordering them to be called one after another. To accomplish this, we send the function that we want to be executed second as a parameter to the first one. Then when the first function is called, it will execute everything and then call the function that was passed as an argument.

const serverDocuments = [];
function getDocuments(){
  setTimeout(()=> {
    for (const document of serverDocuments) {
      console.log(document.name);
    }
  }, 200);
}
function addDocumentOnServer(document, callback){
  setTimeout(()=> {
    serverDocuments.push(document);
      callback();
  }, 1000);
}
addDocumentOnServer({id:9, name:"New Document", size:0.92} ,getDocuments);

 

In this case, we first call the add document function and pass the get documents function as a parameter. This way we can call get documents inside of the add document function at the right time and place. As we said, the solution is simple, but it can get quite messy when longer chains of functions are involved. This is what developers call: Pyramid of doom, Hadouken programming or callback hell. As you can see, developers can be quite creative with names, let’s see how creative they get with solutions for problems that have such extravagant names.

Promises solution

In 2015 developers were delighted to hear that promises became part of the new EC6 standard in JavaScript. No more callbacks. For those that don’t know what promises are, they are objects that wait for data that we requested without knowing when we are going to receive it. With these objects, we can get a much nicer and more elegant solution.

const serverDocuments = [];
function getDocuments(){
  setTimeout(()=> {
    for (const document of serverDocuments) {
      console.log(document.name);
    }
  }, 200);
}
function addDocumentOnServer(document){
  return new Promise((resolve, reject) => {
    setTimeout(()=> {
      serverDocuments.push(document);
      const error = document.name.length > 0 ? false : true;
      if(!error){
        resolve();
      } else {
        reject("Problem adding document on server!");
      }
     }, 1000);
  });
}
addDocumentOnServer({id: 999, name: "", size: 0.92})
  .then(getDocuments)
  .catch(error => console.log(error));

 

Here, we return a new promise from the add document function. We add instructions on what to do when the promise is resolved (the requested data is received) or rejected (data isn’t received, or there is some sort of an error). We can pass the resolve function in the ‘then’ method. When the promise is resolved (the add document function is completed) the get documents function is called. This way, the functions are called in the right order.

 

Async/Await solution

Promises are great. No point in denying that. They were pretty and useful, but after a while, developers started encountering the same problem as with callbacks. Huge chains of ‘then’ and ‘catch’ were appearing. And it wasn’t until 2017 that we got the answer we were looking for all along. Finally, there was a better and more intuitive way to handle promises – async/await.

const serverDocuments = [];
function getDocuments(){
||  -
}
function addDocumentOnServer(document){
  return new Promise((resolve, reject) => {
||  -
  });
}
async function documentsHandling(){
  try{
    await addDocumentOnServer({id: 999, name: "New new new Document", size: 0.92});
    getDocuments();
  } catch(err){
    console.log(err);
  }
}
documentsHandling();

 

The functions are the same as in the previous example. We have the Add and GetAll functions. But this time we are not handling the promise as usual, with ‘then’. Instead, we create an async function. In this function, we can write the keyword await before we call a function. What this basically means is that we are waiting for that function to be finished before the following code is run in the function. Similarly, in the catch block, we can catch any errors that might appear as a result of rejected promises.

It’s difficult to tackle problems like this, especially when new JavaScript features, in this case, async/await, are not fully supported yet. However, it is clear that these are steps in the right direction. You can try it out and see if this solution suits your needs and preferences. 

If, by any chance, you are working on an older project or need to support older browsers, you can try bluebird.js. It is a library that contains all of the above-mentioned features. To keep track of browser support for all JavaScript features you can visit caniuse.com. Hope these tools and features help you create an easier and more intuitive code for handling requests and data.

Category
on December 17, 2018