Callback vs Promise vs async/await
Why there is a multiple approach for handling asynchronous operations in Javascript? How do I choose the best fit for my use case? Lets deep dive to discuss on this topic. Even though myriads of post available on this topic. This story is intended to explain the concepts with simplified examples. If you are already aware of this concepts then its a refresher for you.
Callback
The callback is a function which is passed as a parameter to another function. The callback function is then executed perform some action. There are two types of callback function, they are:
- Synchronous Callback Function
The synchronous callback functions are function which executes synchronously with out any blocking operation.
In the following example, the function sayHello
is invoked with text “Hello” and print
function acts as a callback function. From the sayHello
function, the text value is propagated to the callback function (print) and response is printed.
function print(data) {
console.log(data)
}function sayHello(text, callback) {
callback(text)
}sayHello("Hello", print)
2. Asynchronous Callback
The asynchronous callback functions are functions passed as an parameter for asynchronous operation and gets invoked once the operation completed.
The setTimeout
is one of the example for asynchronous callback. The print
function is passed as a callback and it shall be invoked after 1 second
function print() {
console.log("Timeout End")
}
// The callback function print shall be invoked after 1 second
setTimeout(print, 1000)
Pitfall of Callback Function
- Nested callbacks can be cumbersome and hard to read (i.e. “callback hell”).
- Failure callbacks need to be called once for each level of nesting
Order Processing Psuedocode using Callback
The following order processing psuedocode illustrate the above mentioned pitfall of callback function. Its also an example of callback hell.
// Order Processing Workflow uisng callback
getUserInfo(userId, (error, userInfo)=> {
getProductQuantity(prouctId, (error, prouctInfo)=> {
const orderInfo = { userInfo, prouctInfo }
processPayment(orderInfo, (error, paymentInfo) => {
shipment(orderInfo, (error, shipmentInfo) => {
// Do shipment notification
})
})
})
})
Here, in every stage the error needs to be handled and propagated to caller function. Assume how the snippet will look like when the number of nesting level increases.
In order to mitigate the problems caused by old-school callback style. The Promise
feature is introduced in Javascript.
Promise
The Promise object represents the eventual completion of an asynchronous operation, and its responding value. A promise should be in one of the following three states,
- pending — When the promise is initialized, initial state
- fulfilled — When the successful completion of Promise
- rejected — When the operation failed
The callback function needs to be passed to the Promise constructor to resolve or reject on completion of asynchronous event. In the following example the setInterval
callback is wrapped as a Promise and then function is invoked to resolve the response
const setIntervalPromisified = (interval) => {
const callback = (res, rej) => {
setInterval(()=> {
res(`Fullfilled in ${interval} ms`)
}, interval)
}
return new Promise(callback)
}const response = setIntervalPromisified(1000)
console.log(response)
response.then(console.log)
Execution Output
Promise { <pending> }
Fullfilled in 1000 ms
Order Processing Psuedocode using Promise
Refactor the above mentioned callback approach using Promise. Implement Promise chaining using then()
function to handle the asynchronous operations sequentially.
// Order Processing Workflow uisng callback
getUserInfo()
.then(() => getProductQuantity())
.then(() => processPayment() )
.then(() => shipment() )
.then(() => {
// Do notification
} )
.catch(() => handleError())
Observation
- Here the error handling is controlled using single catch block
- Promising chaining is implemented to handle async operation sequentially
Is there any option to simplify the syntax of Promise and make it look like a synchronous programming style ?
async/await
The async/await is a syntactical sugar of the Promise. The async function contains zero or more await keyword. The async/await make the code look like a synchronous programming style, when the await function is invoked the code execution gets blocked and the next line of executions starts only when the await operation completed.
const initTimer = async (interval) => {
const response = await setIntervalPromisified(interval)
console.log(response)
}
initTimer(1000)
Here in the example, the await keyword block the execution till the setIntervalPromisified
function resolves the response.
The error handling is achieved by using the try catch block, The rejected promised is thrown into catch block.
Order Management Psuedocode using async/await
The Promise based approach is refactored to synchronous programming style. Here the execution of function is blocked and runs sequentially one by o.
const processOrder = async () => {
try {
await getUserInfo()
await getProductQuantity()
await processPayment()
await shipment()
} catch (error) {
// handle error
}
}
Pitfall in async/await
Due to synchronous nature of async/await parallel processing cannot be achieved directly. But it can be achieved by using different pattern.
Parallel Processing in async/await
In the following example`, the function slowAsyncOperation
executes the setIntervalPromisified
sequentially and it took 3 seconds for the completion of the function. In the function fastAsyncOperation
the setIntervalPromisified
function is triggered and initialized to a variable and then awaited for response. So this function run parallel get completed in 1 seconds
const slowAsyncOperation = async () => {
console.time("slowAsyncOperation")
await setIntervalPromisified(1000)
await setIntervalPromisified(1000)
await setIntervalPromisified(1000)
console.timeEnd("slowAsyncOperation")
}// Parallel Processing
const fastAsyncOperation = async () => {
console.time("fastAsyncOperation")
let setIntervalPromisified1 = setIntervalPromisified(1000)
let setIntervalPromisified2 = setIntervalPromisified(1000)
let setIntervalPromisified3 = setIntervalPromisified(1000) await setIntervalPromisified1
await setIntervalPromisified2
await setIntervalPromisified3
console.timeEnd("fastAsyncOperation")
}
Execution Time Taken
fastAsyncOperation: 1015.432ms // Parallel Processing
slowAsyncOperation: 3032.304ms // Synchronous Processing
Conclusion
Based on our use case we can prefer any one of this approach. Since async/await is wrapped on top of Promise, all of the Promise related functionalities are supported within it. So when comparing callback with Promise, Promise has move advantages than callback. Listing out few of them;
- Single Error Propagation using catch block
- Overcome callback hell using Promise Chaining or async/await/
- Implement parallel processing using
Promise.all()
. - Promise supports few other static methods like
(race(), allSettled() and any())
which shall be very usefully on need basic.
Hope this story would have helped you in refreshing the asynchronous handling concepts in Javascript. Please feel free to share your comments, suggestions or queries.