javascript
Callbacks, Promises and Async/Await
Callbacks, promises and async
/await
keywords, are just different ways to work with asynchronous code in JavaScript.
When you work with asynchronous code, you must deal with new problems that synchronous code doesn’t have. For example, when you need to get data from a server, you have to wait for the server to respond. In that case, one of the first solutions was to use callbacks.
Callbacks
DEF: A callback is just a function that is passed to another function as an argument and is executed after some operation is complete.
Imagine that we want to get data from a server, and we want to console.log()
it after. We cannot do it in a synchronous way, because we don’t have the data yet!
So, we wrap the call in a getDataFromServer()
function, defining a callback: Function
param that will be executed after the request is completed.
This allows to pass any function as a callback, knowing that the logic will be executed after we got the data!
function getDataFromServer(endpoint, callback) {
const xhr = new XMLHttpRequest()
xhr.open('GET', endpoint)
xhr.onload = () => {
if (xhr.status === 200) {
callback(null, xhr.responseText)
} else {
callback(new Error('Request failed'))
}
}
}
// The last param is the callback
// callback: (err: Error, data: any) => void
getDataFromServer('/users', (err, data) => {
if (err) {
// manage error
console.log(err)
} else {
// manage data
console.log(data)
}
})
What is happening here?
- We defined a function called
getDataFromServer
that takes two arguments:endpoint
andcallback
.
Endpoint
is the endpoint of the server we want to get data from.
callback
is the function that will be called when the request is completed.
- When the
xhr.onload
event is triggered, we execute thecallback
function passing an error or the data.
By this way, we can get data from the server without worrying about the request status, because we can handle both cases with inside callback. Notice that we define the logic of the callback in the function call and not in the function definition, so we can reuse the same function for different requests with different implementations.
The problem: Callback Hell
There is a problem with this approach. If we have a lot of callbacks nested, it can be hard to read and understand.
For example, let’s create a 3-step process with callbacks. In this case, our process will be:
- Get a cup
- Fill the cup with coffee
- Drink the coffee
First, we define our 3 functions:
const getCup = (user, cb) => {
console.log(`${user} got a cup`)
cb('coffee')
}
const fillCup = (liquid, cb) => {
console.log(`Filling the cup with ${liquid}`)
cb('drink')
}
const executeAction = (action, cb) => {
console.log(`To ${action} the cup`)
cb('Done!')
}
Now, we can call the functions in the correct order:
getCup('Dawichi', liquid => {
fillCup(liquid, action => {
executeAction(action, result => {
console.log(result)
})
})
})
As you can see, we start appreciating the problem: we are nesting callbacks… and only with 3 steps! Imagine after 10, hard to read.
This is a problem because we can’t understand what is happening in an easy way. If we have a lot of nested callbacks, it can be extremely hard to maintain and understand later.
That’s why we have promises.
Promises
Promises are a way to handle asynchronous code in JavaScript. It’s the new standard for asynchronous code in JavaScript, as most of the Node.js APIs are being changed from callbacks to promises. They were included in ES6, but the idea was used previously for many libraries like Q, Bluebird or JQuery.
The idea of promises is to resolve
or reject
a value at some point in the future, so meanwhile we just Promise
that the value will be available in a certain period.
Let’s see how it works:
const getData = () => {
return new Promise((resolve, reject) => {
// Let's simulate a request to a server with 1s delay
const error = false
const data = { name: 'Dawichi', age: 25 }
// After 1s, we resolve the promise with the data
setTimeout(() => {
if (error) {
reject(new Error('Request failed'))
} else {
resolve(data)
}
}, 1000)
})
}
Now, if we call the function and we print the return value directly, we will get a Promise
object.
const data = getData()
console.log(data) // Promise { <pending> }
This is the first step of the promise. We call the function and we get a promise of a value, and to get the actual value, we have to call the then
method.
getData().then(data => {
console.log(data) // { name: 'Dawichi', age: 25 }
})
What is happening here?
- We call the
getData
function. - We get a promise of a value.
- We call the
then
method with a callback function. - The callback function is called with the value of the promise.
This is really useful because it allows us to write the code in a more readable way (with other perks).
Let’s see the 3-step coffee break process with promises:
const getCup = () => {
return new Promise((resolve, reject) => {
console.log('Getting a cup')
resolve('coffee')
})
}
const fillCup = liquid => {
return new Promise((resolve, reject) => {
console.log(`Filling the cup with ${liquid}`)
resolve('drink')
})
}
const executeAction = action => {
return new Promise((resolve, reject) => {
console.log(`To ${action} the cup`)
resolve('Done!')
})
}
The 3 functions are now encapsulated in promises. For sure this looks worse now, as de function definition have been increased. But then it comes the perks of promises: consume the function.
We call them in the correct order:
getCup()
.then(liquid => fillCup(liquid))
.then(action => executeAction(action))
.then(result => {
console.log(result)
})
This is a lot easier to read and understand!
But it doesn’t end with a .then
method. There are other methods like catch
and finally
, which can be used in multiple situations to make our code more readable.
Let’s see catch
and finally
:
getCup()
.then(liquid => fillCup(liquid))
.then(action => executeAction(action))
.then(result => {
console.log(result)
})
.catch(err => {
console.log(err)
})
.finally(() => {
console.log('Everything is done!')
})
Now, if any error happens along the process, it will be handled by the catch
method, allowing us to handle the error.
This is a very useful feature, but there is still a last beautiful tool: async/await
.
Async/Await
The async/await
syntax is a new way to write asynchronous code in JavaScript. It was introduced in ES2017, and it allows to consume our asynchronous functions in a much more readable way in certain situations.
Let’s change our last 3-step example into an async/await version:
NOTE: As
async/await
is based in the idea of that create an ‘async function’ and ‘await values’, we are going to encapsulate ourgetCup()
call into aprocess()
function in both examples.
// Process function using 'then'
const process = () => {
getCup()
.then(liquid => fillCup(liquid))
.then(action => executeAction(action))
.then(result => {
console.log(result)
})
}
// Process function using 'async/await'
const process = async () => {
const cup = await getCup('Dawichii')
const drink = await fillCup(cup)
const result = await executeAction(drink)
console.log(result)
}
This allows to write code in a less nested way in some situations and is being really used when we have a lot of complex then
logic.
The bad part is that to handle errors we have to use a try/catch
block.
// Process functino using 'then'
const process = () => {
getCup()
.then(liquid => fillCup(liquid))
.then(action => executeAction(action))
.then(result => {
console.log(result)
})
.catch(err => {
console.log(err)
})
}
// Process function using 'async/await'
const process = async () => {
try {
const cup = await getCup('Dawichii')
const drink = await fillCup(cup)
const result = await executeAction(drink)
console.log(result)
} catch (err) {
console.log(err)
}
}
So in small cases it creates a lot of code that could be simpler with then
, so it depends on the situation what is better.
Conclusion
Right now, to work with asynchronous code is easier than ever. We have extremely useful tools to handle all the use cases and preferences, depending on what we need.
The most important thing is to understand the concepts behind each tool, and then choose the one that fits better in each situation!