Callback vs Promise vs async/await

Ashok JayaPrakash
5 min readAug 16, 2020

--

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:

  1. 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,

  1. pending — When the promise is initialized, initial state
  2. fulfilled — When the successful completion of Promise
  3. 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

  1. Here the error handling is controlled using single catch block
  2. 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;

  1. Single Error Propagation using catch block
  2. Overcome callback hell using Promise Chaining or async/await/
  3. Implement parallel processing using Promise.all().
  4. 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.

--

--

Ashok JayaPrakash

Software Engineer, Open Source Enthusiast. Javascript, Node.js