Let’s say you have a function that will print a string after a random amount of time:
function writeText(string){
setTimeout(
() => {
console.log(string)
},
Math.floor(Math.random() * 100) + 1
)
}
Let’s try to print the letters A, B, C in that order:
function writeAllText(){
writeText("A")
writeText("B")
writeText("C")
}
writeAllText()
You will notice that A, B, and C print in a different and random order each time you call writeAllText! This is because these functions are asynchronous. Each function gets executed in order, but each one is independent with it’s own setTimeout. They won’t wait for the last function to finish before they start. This is super annoying, so let’s fix it with a callback.
A callback is a function that is passed to another function. When the first function is done, it will run the second function
function writeText(string, callback){
setTimeout(
() => {
console.log(string)
callback()
},
Math.floor(Math.random() * 100) + 1
)
}
You can see that is is super easy to modify the original function to work with callbacks.
Again, let’s try to print the letters A, B, C in that order:
function writeAllText(){
writeText("A", () => {
writeText("B", () => {
writeText("C", () => {})
})
})
}
writeAllText()
Well, the code is a lot uglier now, but at least it works! Each time you call writeAllText, you get the same result.
The problem with callbacks is it creates something called “Callback Hell.” Basically, you start nesting functions within functions within functions, and it starts to get really hard to read the code.
Promises try to fix this nesting problem. Let’s change our function to use Promises
function writeText(string){
return new Promise((resolve, reject) => {
setTimeout(
() => {
console.log(string)
resolve()
},
Math.floor(Math.random() * 100) + 1
)
})
You can see that it still looks pretty similar. You wrap the whole function in a Promise, and instead of calling the callback, you call resolve (or reject if there is an error). The function returns this Promise object.
Again, let’s try to print the letters A, B, C in that order:
function writeAllText(){
writeText("A")
.then(() => {
return writeText("B")
})
.then(() => {
return writeText("C")
})
}
writeAllText()
This is called a Promise Chain. You can see that the code returns the result of the function (which will be a Promise), and this gets sent to the next function in the chain.
The code is no longer nested but it still looks messy!
By using features of arrow functions, we can remove the “wrapper” function. The code becomes cleaner, but still has a lot of unnecessary parenthesis:
function writeAllText(){
writeText("A")
.then(() => writeText("B"))
.then(() => writeText("C"))
}
writeAllText()
Await is basically syntactic sugar for Promises. It makes your asynchronous code look more like synchronous/procedural code, which is easier for humans to understand.
The writeText function doesn’t change at all from the promise version.
Again, let’s try to print the letters A, B, C in that order:
async function writeAllText(){
await writeText("A")
await writeText("B")
await writeText("C")
}
writeAllText()
Yeah…. MUCH better!
You might notice that we use the “async” keyword for the wrapper function writeAllText. This let’s JavaScript know that we are using async/await syntax, and is necessary if you want to use Await. This means you can’t use Await at the global level; it always needs a wrapper function. Most JavaScript code runs inside a function, so this isn’t a big deal.
The writeText function doesn’t return anything and is independent, all we cared about was the order. But what if you wanted to take the output of the first function, do something with it in the second function, and then pass it to the third function?
Instead of printing the string each time, let’s make a function that will concatenate the string and pass it on.
function addString(previous, current, callback){
setTimeout(
() => {
callback((previous + ' ' + current))
},
Math.floor(Math.random() * 100) + 1
)
}
in order to call it:
function addAll(){
addString('', 'A', result => {
addString(result, 'B', result => {
addString(result, 'C', result => {
console.log(result) // Prints out " A B C"
})
})
})
}
addAll()
Not so nice.
function addString(previous, current){
return new Promise((resolve, reject) => {
setTimeout(
() => {
resolve(previous + ' ' + current)
},
Math.floor(Math.random() * 100) + 1
)
})
}
And in order to call it:
function addAll(){
addString('', 'A')
.then(result => {
return addString(result, 'B')
})
.then(result => {
return addString(result, 'C')
})
.then(result => {
console.log(result) // Prints out " A B C"
})
}
addAll()
Using arrow functions means we can make the code a little nicer:
function addAll(){
addString('', 'A')
.then(result => addString(result, 'B'))
.then(result => addString(result, 'C'))
.then(result => {
console.log(result) // Prints out " A B C"
})
}
addAll()
This is definitely more readable, especially if you add more to the chain, but still a mess of parenthesis.
The function stays the same as the Promise version.
And in order to call it:
async function addAll(){
let toPrint = ''
toPrint = await addString(toPrint, 'A')
toPrint = await addString(toPrint, 'B')
toPrint = await addString(toPrint, 'C')
console.log(toPrint) // Prints out " A B C"
}
addAll()
And in order to call it:
function addAll(){
addString('', 'A')
.then(result => {
return addString(result, 'B')
})
.then(result => {
return addString(result, 'C')
})
.then(result => {
console.log(result) // Prints out " A B C"
})
}
addAll()
Yeah. SO MUCH BETTER.
Callback are the only way by which JavaScript achieves asynchronocity, there is no other way as JS is single threaded language. Promises and Async|await are syntactical sugar on top of it to make our code readable. Back of the hood all are dealing in callbacks.