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.
/* 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.
- async function will always return a promise
- 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: