Callbacks, Promises and Async/Await

Callbacks

A callback function is a function(F1) passed into another function(F2) as an argument, which is then invoked inside the function(F2) to complete some kind of action.

  • In the end callbacks are nothing but functions.
  • But there are some best practices/standards whatever you call it. Ex: first parameter should be always error.
/* Here is an callback example */

/**
 * Here handleUser is a callback function for request function
 */

function handleUser(err, response) {
    if (err) {
       /*
        log error
        do required error handling
        */
    }
    /* continue with further processing */
}

function request(url, cb) {
  /* get data */
  let data = {}
  cb(null, data);
}

request('https://api.github.com/users/rrkjonnapalli', handleUser)

Promises

  • Even though we can use callbacks, there are times where they can become pain in the #.

Assume we need to fetch user and his followers and then followers of the followers, how do we do that?

/*
Here is the sample output
{
    login: 'xyz',
    followers: [
        {
            login: 'f1',
            followers: [
                ...
            ]
        },
        {
            login: 'f2',
            followers: [
                ...
            ]
        }
    ]
}
*/

Let's try to write the solution in callbacks

/* callback-task.js */

/* returns a base 36 string */
const getLogin = (seed) => (Math.round(Math.random() * 10000000)).toString(36) + seed.toString(36);

/* returns a random no from 0-10 */
const getSeed = () => Math.round(Math.random() * 10);

/**
 * @param {string} login 
 * 
 * Success callback will have the data with following syntax
 * {
 *   login: 'string',
 *   followers: [
 *     'string'
 *      ....
 *   ]
 * }
 */
function getData(login, cb) {
  let seed = getSeed();
  let followers = [];
  while (seed--) {
    followers.push(getLogin(seed));
  }
  cb(null, {
    login,
    followers
  };
}


/**
 * 
 * @param {Array<string>} list <List of logins>
 * @param {Number} i <Current index of List>
 * @param {Array<Object>} result
 * @param {Function} cb 
 * Success callback will have the data with following syntax
 * [
 *   {
 *     login: 'string',
 *     followers: [
 *       'string'
 *        ....
 *     ]
 *   },
 *   ...
 * ]
 */
function getFollowers(list, i, result, cb) {
  if (i >= list.length) return cb(null, result);
  let login = list[i];
  return getData(login, (err, data) => {  /* waiting till we get the result from getData */
    if (err) return cb(err);
    result.push(data);
    return getFollowers(list, i + 1, result, cb);
  });
}



function callbackSolution(login, cb) {
  getData(login, (err, data) => {
    if (err) return cb(err);
    getFollowers(data.followers, 0, [], (fErr, fData) => {
      if (fErr) return cb(fErr);
      data.followers = fData;
      cb(null, data);
    })
  });
}

callbackSolution('xyz', (err, data) => {
  console.log(err);
  console.log(data);
});

/*
output:
{
  login: 'xyz',
  followers: [
    { login: '1iqt48', followers: [Array] },
    { login: '4b9dt7', followers: [Array] },
    { login: '5nl0w6', followers: [Array] },
    { login: '4odoc5', followers: [Array] },
    { login: '4q1dk4', followers: [Array] },
    { login: '1w4tb3', followers: [Array] },
    { login: '538ru2', followers: [Array] },
    { login: '1499q1', followers: [Array] },
    { login: '2ybvn0', followers: [Array] }
  ]
}
*/

it may not be a good example, but try to understand what's happening there. we've main callbackSolution function at the end.

Procedure:

  • Getting the followers of give user
  • Calling getFollowers function with acquired result followers
  • For each follower we are fetching followers
  • Once we reach the end of the list calling the callback with the followers list.

Here we're asking for followers and followers of followers for user/login "xyz". You can try and run it in your machine.

I don't care if you do not understand the above code callback-task.js.

But you should understand from now on wards.

With promise either we can have either resolve or reject.

If a promise resolves then it's a success, if it's a rejection then it's a failure.

/* promise-play.js */
let rejectPromise = Promise.reject(new Error('Error x'));
let resolvePromise = Promise.resolve({ ok: true, message: 'Success' });


/**
 * Below two declarations are same as above
 * Choose based on your requirements
 */

let invokeResolvePromise = new Promise((resolve) => {
  return resolve({ ok: true, message: 'Invoked Success' });
});

let invokeRejectPromise = new Promise((resolve, reject) => {
  if (true) {
    return reject(new Error('Invoked Error x'));
  }
  return resolve({ ok: true, message: 'Success' });
});

rejectPromise
  .then(result => {
    /* Here result will be undefined */
  }).catch(err => {
    /* We will have Error x here  */
  });

resolvePromise
  .then(result => {
    /* 
    We will have result as {ok: true, message: 'Success'}
    do procession
    */
  }).catch(err => {
    /*
    Unless there's an error in the above processing we won't get
    any error here.
    */
  });

/* Lets play with promises */

resolvePromise
  .then(result => {
    console.log(result); /* {ok: true, message: 'Success'} */
    return rejectPromise;
  })
  .then(console.info)
  .catch(console.error) /* Error x */
  .then(result => {
    console.log(result); /* undefined */
    return invokeResolvePromise;
  })
  .then(result => {
    console.log(result); /* {ok: true, message: 'Invoked Success'} */
    return invokeRejectPromise;
  })
  .catch(console.error) /*Invoked Error x */
  .then(() => {
    return resolvePromise
  })
  .then(console.info); /* {ok: true, message: 'Success'} */

Let's try to solve the above problem of fetching followers with promises. Here I don't want to write above code again, node gave us a utility to convert callbacks to promises.

Util.Promisify

/* promise-task.js */
const util = require('util');

/* get the code from above callback-task.js and comment the callbackSolution calling block */

const getDataPromise = util.promisify(getData);

function promiseSolution(login) {
  return getDataPromise(login)
    .then(data => Promise.all([
      Promise.resolve(data),
      ...data.followers.map(f => getDataPromise(f))
    ]))
    .then(data => { 
      /*
      waiting till we get the results of getDataPromise calls for
      all followers.
      */
      let [result, ...followers] = data;
      result.followers = followers;
      return result;
    })
}

promiseSolution('xyz')
  .then(console.log)
  .catch(console.error);

Async/Await

async there is popular npm package with this name to handle different scenarios in callbacks you can take a look.

But here we are not talking about that. async and await are now two reserved js keywords.

There are two things you need to keep in mind while using async/await.

  1. async function will always return a promise
  2. await should always be in an async function.

Let's play

/* async-await-play.js  */
/* you can take the promises from promise-play.js */

async function awaitHandler() {
  /* we're sure that this always resolves */
  result = await resolvePromise;
  console.log(result); /* { ok: true, message: 'Success' } */

  /* we're sure that this always resolves */
  result = await Promise.all([resolvePromise, invokeResolvePromise]);
  console.log(result); /* [{ ok: true, message: "Success" }, { ok: true, message: "Invoked Success" }] */

  /*
  result = await rejectPromise; // wrong handling because it's a rejection
  console.log(result);
  */

  try {
    result = await rejectPromise;
    console.log(result);
  } catch (error) {
    console.log(error); /* Error x */
  }

  result = await rejectPromise.catch((err) => {
    console.log(err); /* Error x */
    return 'Await Handler Error Handling 2';
  });
  console.log(result); /* Await Handler Error Handling 2 */

  try {
    result = await Promise.all([resolvePromise, rejectPromise, invokeResolvePromise, invokeRejectPromise]);
    return console.log(result);
  } catch (error) {
    return console.error(error); /* Error x */
  }
}

async function asyncHandler() {
  return { ok: true, message: 'Async Handler' };
}
async function asyncErrorHandler() {
  throw new Error('Async Error x');
}
async function nestedAsyncHandler() {
  return asyncHandler();
}

awaitHandler()
  .then(() => { })
  .catch(() => { });

asyncErrorHandler()
  .then(console.log) /* { ok: true, message: 'Async Handler' } */
  .catch(console.error);

asyncHandler()
  .then(console.log)
  .catch(console.error); /* Async Error x */

nestedAsyncHandler()
  .then(console.log) /* { ok: true, message: 'Async Handler' } */
  .catch(console.error);

Playtime is over.

Let's solve the above task in async/await.

/* async-await-task.js */
/* get the code from promise-task.js and comment the promiseSolution calling block. */

async function asyncAwaitSolution1(login) {
  let data = await getDataPromise(login).catch(error => ({ error }));
  if (data && data.error) throw data.error;
  let followers = [];
  for (let f of data.followers) {

    /* waiting for each follower result */

    let follower = await getDataPromise(f).catch(error => ({ error }));
    if(follower && follower.error) throw follower.error;
    followers.push(follower);
  }
  data.followers = followers;
  return data;
}

asyncAwaitSolution1('xyz')
  .then(console.log)
  .catch(console.log);

async function asyncAwaitSolution2(login) {
  let data = await getDataPromise(login).catch(error => ({ error }));
  if (data && data.error) throw data.error;

  /* waiting all followers result */

  let followers = await Promise
    .all(data.followers.map(f => getDataPromise(f)))
    .catch(error => ({ error }));
  if (followers.error) throw followers.error;
  data.followers = followers;
  return data;
}

Here we have two methods solving the same problem. Both will return the correct result. You need to choose between two, depends on your requirement. For example

  • sometimes you may want to process few simple db operations then you can use all at a time approach.
  • sometimes if you have multiple complex db operations to be done and if you use the all at a time approach it may affect the performance of your db.

Choose what best for you.

References: