I Promised Myself not to Callback again!

I promised myself not to callback again but I’m breaking this promise so I went “out of sync” (Async) to await the results.

Mohamed Sayed
6 min readMay 2, 2019

Hello 👋, I have been writing NodeJS for the past 4 years, I wrote a lot of “shitty” code especially in NodeJS, as when I started using it, I was coming from PHP and Python where callbacks were simply not there, so I wrote shitty code in the beginning, at the end and even now.

I worked with callbacks for around 2 years and then I heard about Promises which was interesting so I thought let’s give it a try.

Started to integrate it in my code but it felt off; As my knowledge of NodeJS event loop, I wondered how promises works, so I searched around to understand more how it works, what I found is a promise is just a sugared wrapped callback.

After around a year of using Promises with sometimes a mix of callbacks, Async/Await arises. I really struggled, in the beginning, to use it, despite the fact that it looks really good and finally, I won’t make 10 chains of promises like this one.

Promis
.then( () => Promise2 )
.then( () => Promise3 )
.then( () => Promise4 )
.then( () => Promise5 )
...
.then( () => Promise10 )
.catch( err => {
// which error should I catch :(
})

I can only Await for those promises which will make the code more readable, but then I researched and found that Async/Await is a sugar-coated Promise, right now we have a double layer of sugar coating.

Lately, I was working with my teammates to refactor old code and we moved to Async/Await so some of the code that I was going through before pushing to staging was something like:

const function = async () => {
let res = await promise();
return res;
}

I didn’t like this code in the beginning, so I was editing this code to something like this:

const function = () => {
return promise();
}

My mind kept looping around why it felt like this. That’s when I decided to transform those feelings into actual numbers and I thought up a little experiment to do so.

In the following lines, we will talk about the difference execution time between

Callback + 🍭 = Promises + 🍡 = Async/Await

and will have some takeaways.

Playground

Env

MacOS Sierra on MacBook Pro 13 inch 3.5 core i7 with 16GB of ramNodejs version 8.11.3

Callback

Callbackconst callback = callback => {
setTimeout( () => {
return callback('Hello')
}, 100)
}
const callbackTest = () => {
console.time('Test callback')
callback( res => {
console.timeEnd('Test callback')
})
}
callbackTest()

If you executed this code using nodejs v8.11.3 100 times using the following command and pipe the logs to a file so we can extract the data.

for i in {1..100}; do node callback.js; done > callback.log

The execution time varies between 100.83–107.713ms

Promises

const promise = () => {
return new Promise( (resolve, rejcet) => {
setTimeout( () => {
return resolve('Hello')
}, 100)
})
}
const promiseTest = () => {
console.time('Test Promise')
promise()
.then( res => {
console.timeEnd('Test Promise')
})
}
promiseTest()

using the same command

The execution time varies between 100.898–108.017ms

Async/Await

Two ways to write it:

First:

const promise = () => {
return new Promise( (resolve, rejcet) => {
setTimeout( () => {
return resolve('Hello')
}, 100)
})
}
const function1 = () => {
return promise();
}
const asyncTest = async () => {
console.time('Test async')
let res = await function1()
console.timeEnd('Test async')
}
asyncTest()

The execution time varies between 101.201–108.067ms, the results are similar to the above promise trial

Second:

const promise = () => {
return new Promise( (resolve, rejcet) => {
setTimeout( () => {
return resolve('Hello')
}, 100)
})
}
const function2 = async () => {
let res = await promise();
return res;
}
const asyncTest = async () => {
console.time('Test async')
let res = await function2()
console.timeEnd('Test async')
}
asyncTest()

The execution time varies between 100.754–109.822ms, which is a little more than the first way.

To get a full view and combine everything:

We can see for sure that callback is the winner and Async 2 is the loser.

On last proof is run this code and you will notice that callback log the output first

const promise = () => {
return new Promise( (resolve, rejcet) => {
setTimeout( () => {
return resolve('Hello')
}, 100)
})
}
const callback = callback => {
setTimeout( () => {
return callback('Hello')
}, 100)
}
const function1 = () => {
return promise();
}
const function2 = async () => {
let res = await promise();
return res;
}
const promiseTest = async () => {
console.time('Test Promise')
promise().then( res => {
console.log(res, 'Promise')
console.timeEnd('Test Promise')
})
}const callbackTest = () => {
console.time('Test callback')
callback( res => {
console.log(res, 'Callback')
console.timeEnd('Test callback')
})
}
const async1Test = async () => {
console.time('Test async1')
let res = await function1()
console.log(res, 'async/await 1')
console.timeEnd('Test async1')
}
const async2Test = async () => {
console.time('Test async2')
let res = await function2()
console.log(res, 'async/await 2')
console.timeEnd('Test async2')
}
async1Test()
async2Test()
promiseTest()
callbackTest()

The out will always be something like this

Hello Callback
Test callback: 105.901ms
Hello Promise
Test Promise: 106.658ms
Hello async/await 1
Test async1: 107.433ms
Hello async/await 2
Test async2: 106.969ms

The callback function will always execute faster, that makes a lot of sense.

As I Promised and you did Await for it here is the takeaways:

1. Don’t ever never use await for a function inside an async function

function 2 ❌

const function2 = async () => {
let res = await promise();
return res;
}

function 1 âś…

const function1 = () => {
return promise();
}

in function 2 you invoke 2 promises to get the results of promise() function the first one is

await promise()

the second one is

async () => {...}

so this takes more time to execute but function1 only returns one promise which is the main promise, if you are gonna do anything on the data before returning it use function2 otherwise use function1 and know what you will gain and what you will lose.

2. Callbacks are not a bad thing in 2019

Callbacks are still valid in 2019 and still faster, so I recommend using callbacks when there is no chance for callback hell, and when you don’t wanna lose around 0.05–1ms per promise function invocation, so imagine you are looping over an array how long you will delay your user.

0.05*array.length ms

So bear in mind everything to make your code faster.

3. Trust your guts.

Appreciate your guts test them to prove you are right or wrong.

--

--

Mohamed Sayed

A software engineer who is changing how people move one line at a time. @flixbus @transportforcairo @thed. GmbH