JavaScript: Mapping and Wrapping Classic Callback Functions to Promises and Async / Await

Lucas Jellema 1

Callback functions used to be very common in JavaScript and Node applications. Out of necessity. And with Callback functions came unclear program flows, parallel realities, nested complexity and more.

ECMAScript introduced Promises several years ago and followed that up with the neat await and async syntax. This allows us to have asynchronous execution as part of the regular program flow. Everything orderly neatly and sequentially.

However, there are still many libraries that work with old-style callback functions. Would it not be nice if we can somehow make those callback functions go away and do everything async & await? I am sure many articles discuss this already and I have read a few. However, they were not as clear to me as I believe they could have been. So for my own sake, I have tried to create a few very simple examples to help me easily address the callback function in future circumstances.

An almost frivolous example to get started. Here the callback function is shown in the red rectangle:

image

function callme(name, callback) {
   const message = "Hello "+ name
   callback(message)
}

callme("Lucas", (msg)=> {console.log(`Message from callme: ${msg}`)})

There is no need really here for using a callback function, as there is no asynchronous processing. But it serves as an example. The following code snippet is equivalent. However, the calling code does not pass in a callback function: it awaits the response and then does whatever the callback function was doing before.

async function callmeAsync(name) {
    const message = "Hello "+ name
    return new Promise((resolve,reject) => {resolve(message)})
 }
 
async function becalled() {
   const msg =  await callmeAsync("Lucas")  
   console.log(`Message from callmeAsync: ${msg}`)
} 

becalled()
image

The trick is in using the Promise: the function returns a Promise and await knows how to deal with a Promise: it waits for the promise to be resolved (or rejected) and produced the result of the resolution (and in this vase assigns it to variable msg. The Promise in this case is quite underwhelming: it immediately resolves and return the message.

Note that I introduced a function becalled() that was not there before. This is out of necessity: await can only be used in functions that have been designated async functions. 

The outcome of both constructions is the same:

image

A classic example of using a callback function is in setTimeout when a function is scheduled for execution at a later moment. Here the callback function to be executed after 1500 ms is shown in the red rectangle.

image

function moveOn() {
    console.log("After pausing, now move on");
}

setTimeout(function () { moveOn() }, 1500)

Using async and await, this code could be written as follows:

image

async function pause(time) {
    return new Promise((resolve, reject) => 
        setTimeout( () => {return resolve() }, time)
    )
}

async function doAfterPause() {
    await pause(1500)
    moveOn()
}

doAfterPause()

Again, the logic of the [former] callback function is shown in the rectangle.

Again, a Promise is used to wrap the asynchronous operation (setTimeout) and the callback function does nothing but immediately resolve. In this case, no value is produced by resolve(). The await will not continue with invoking moveOn() until the call to function pause() has produced a value, which happens after the timer expires and the Promise resolves.

File Reading

Reading files in Node JS is a classic example of an asynchronous operation (even though a blocking, synchronous file read function is available). A typical example of reading a file uses a callback function that is invoked as soon as the asynchronous file read operation has been completed:

image

The callback function is highlighted.

// non blocking file read:
function readFileNonBlock( filename, callback) {
  fs.readFile("./"+filename, 'utf8', callback);
}

readFileNonBlock('package.json', (err,data) => 
    {
      console.log("Data received from file "+data)
    }
);

In a similar way as shown before, we can wrap the classic callback construction and make it work with more modern await:

image

The logic to be executed after the file has been read is highlighted in the red rectangle.

The readFileAsync() function returns a Promise and await will wait for that Promise to resolve. The callback function passed into fs.readFile() is the same we have seen before: a function that just resolves [the Promise] with the result passed to the callback function.

async function readFileAsync(filename) {
    return new Promise((resolve, reject) => {
      fs.readFile(filename, 'utf8', function (err, data) {
        if (err) {
          reject(err);
        }
        resolve(data);
      });
    });
  }

  
async function dealWithFile() {
   const data = await readFileAsync('package.json')
   console.log("Data received from file "+data)
}

dealWithFile()

The callback function is always the same: one that resolves with the result passed to the callback function – or rejects with the error passed to the callback function.

In this example again I had to introduce an async function to be able to use await with a call to the async function readFileAsync()

 

Resources

StackOverflow on File Reading: https://stackoverflow.com/questions/46867517/how-to-read-file-with-async-await-properly

Rohan Paul’s article (Converting JavaScript callbacks to Promise and Async-Await & replacing Async-waterfall method with a Promise)  on Medium https://medium.com/javascript-in-plain-english/converting-javascript-callbacks-to-promise-and-async-await-replacing-async-waterfall-method-with-3c8b7487e0b9

 

One thought on “JavaScript: Mapping and Wrapping Classic Callback Functions to Promises and Async / Await

  1. Well explained issues, quite often I had a problem with understanding reading books but on the example it looks much better. From what I remember kiedsy there was quite a lot of callback hell and I think promise helps in this problem.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Next Post

Somewhat Stateful Serverless Functions on Oracle Cloud Infrastructure - Implementing a DIY Cache

Facebook 0 Twitter Linkedin We call them serverless and we consider them stateless. Functions on Oracle Cloud Infrastructure (and on other cloud platforms). Of course they are neither. They run on servers. And they can carry some state, though largely opportunistic and not reliably. Functions on OCI run inside a […]