Callback is the way asynchrony is built into JavaScript. Something like, “when my dress is ready, here’s my digit, give me a call. I got a place to be”.
But then it became obvious that callback was hell and so many code fell into the pit of hell. This prompted the rise of a saviour for every future code so they don’t go to hell while developers go gaga. A promise for a better future came into the picture. This picture is what hangs on the wall of every “modern” JavaScript developer and marks the beginning of a new era for handling asynchronous flows. This gospel has been widely accepted, but peradventure there are unbelievers out there, was why I had to preach it.
No doubt, dealing with callback was hell, but promises too could be too much to deal with. Do you agree? No, right! Let’s go…
getUser(id)
.then(user => sendSMS(user.telephone))
.then(info => parse(info))
.then(data => res.send(data))
.catch(() => {})
…and even worse, what I call a promise hell.
Confession: I once wrote promises like this, before I sought for more knowledge knowing this here, doesn’t fix the issue with callbacks—not even by an inch.
I didn’t exactly know that whatever is returned from the callback passed to .then
is further
wrapped in a Promise
and returned by the .then
method. Most callbacks return void
and even if
they returned something, it doesn’t always matter to the code invoking them, so they just vanish. My
reasoning was just like this for Promise
callbacks too!
getUser(id).then(user => {
sendSMS(user.telephone).then(info => {
parse(info).then(data => {
res.send(data)
})
})
})
That’s it! Still the same callback hell, but this time, lurking in the shadows of what was meant to be a better future—that same demon trying to possess the saviour.
With this “promise hell” up there, we’d have to do error handling for each promise separately,
adding a .catch
to everyone of them. Hell indeed!
Explicit promises
I termed it explicit Promise
, because the intention to return a Promise
would be clear even to a
layman. Just like the way a function can explicitly return undefined
or void
and/or implicity
return undefined
or void
when no return statement is present.
function greet() {
console.log('Hello')
return undefined
}
function reply() {
console.log('Hi')
}
let greeting = greet() // undefined
let response = reply() // undefined
The intention to return undefined
is clear in the first function greet
, but it would take
someone who is familiar with programming languages to know the function reply
returns undefined
too. So also with Promise
.
function wait(time) {
return new Promise(resolve => {
setTimeout(resolve, time)
})
}
Our intention to return a Promise
in this statement is explicity stated. There is another form
which may not seem as explicit as this, but if you scrutinize quite well, it is as explicit, and
TypeScript can easily make you cognizant of that.
function getUser(id: string) {
const user: Promise<User> = fetch(`${BASE_URL}/people/${id}`).then(res => res.json())
return user
}
If we could explore the implementation of fetch
, we’d realize that it eventually returns a
Promise
, and since our function returns the result of fetch
, we indirectly return a Promise
too.
Implicit promise (async/await)
With Promise
in the picture, the flow of program deviated from what you’d normally expect. Well
this was nothing new, it’s what we’ve known for quite a while. Even with callbacks the flow wasn’t
perfect. This is obvious from the way error handling is done in both promises and callbacks.
Although it was better with promises—the separation of concerns unlike with callbacks—it’s way
better with async
/await
. We can return to the control flow we’ve always known, try
/catch
.
import fs from 'fs'
import fsp from 'fs/promises'
// Error case and success case being handled from the same callback
fs.readFile(filename, (err, result) => {
if (err) {
return console.error(err)
}
// do something with result
})
// Error case and success case handled separately and clear enough
fsp
.readFile(filename)
.then(result => {
// do something with result
})
.catch(err => {
console.error(err)
})
These have deviated from the traditional try
/catch
flow. There need to be a way we can reason
about a program that would once again seem normal to us. And to be honest, the .then
, .catch
flow could be overwhelming when it gets too much.
We were given two brothers, async
/await
to relieve us the work and give us a program flow we can
work with, and a clear syntax same as a normal synchronous code.
const parseJSONFile = async filename => {
try {
const result = await fsp.readFile(filename)
return JSON.parse(result)
} catch (e) {
console.error(e)
}
}
Now we can re-implement the previous getUser
function with a better and clearer syntax. We don’t
have to reach for a variable or response from only inside a callback. Our variables are available
just in the same block of code.
async function getUser(id: string) {
const res: Response = await fetch(`${BASE_URL}/people/${id}`)
const user: User = await res.json()
return user
}
We didn’t explicity return a promise, what we returned here is a User
, but internally in the
JavaScript engine, the async
keyword makes the User
to end up being wrapped in a Promise
and
look just like Promise<User>
.
Promise execution order
Consider these two promises:
// Not preserved
function exec() {
wait(4000).then(() => console.log('Promise 1'))
wait(2000).then(() => console.log('Promise 2'))
return Promise.resolve() // For the sake of having a similar API
}
Now consider the same promise but with async
/await
:
// Preserved
async function exec() {
await wait(4000)
console.log('Promise 1')
await wait(2000)
console.log('Promise 2')
}
These two implementations are different. In the first implementation, the second promise will
resolve before the first. Consequently, 'Promise 2'
will be logged before 'Promise 1'
. In the
second implementation, the order of execution is guaranteed and would be line by line as the first
promise would be await
-ed before any other line continues within the async function. Although the
order of execution can be preserved with a .then
too, but this is worth noting.
Also, the first exec
function will resolve even before the two wait
promises resolve, because
its resultant resolution doesn’t depend on any of the wait
promises. The second exec
function
isn’t resolved until all underlying promises have been successfully await
-ed
Preserving order of execution in .then
function exec() {
return wait(4000)
.then(() => {
console.log('Promise 1')
return wait(2000)
})
.then(() => console.log('Promise 2'))
}
This guarantees that only after the first promise has resolved (await
-ed successfully) would
the second promise start execution. Also, unlike the previous one, this exec
function would not
resolve until all the wait
promises are resolved, since the result of the wait
promise is what
is being returned, its resultant resolution is dependent on the overall wait
promises.
The promise having a non-preserved execution order could be useful in scenarios where the execution of the promises are independent.
Be candid, which syntax seems more appealing to you, the one with
await
or the one with.then
andreturn
?
Converting callback-based asynchronous API to Promise
Many times you will see code trying to make a promise out a callback-based asynchronous API. Many
times this is needed, cause the way a callback and a promise works differ, and we need to make
things uniform and in phase. Some APIs still rely on callback for asynchrony, but this may not work
for you if your code heavily depends on Promise
. Reaching a callback and accessing its value,
obviously can’t be done lazily with an async
/await
. We need to take all ammo out and summon a
full-fledged Promise
.
Look what we did with our wait
function the other time
function wait(time) {
return new Promise(resolve => {
setTimeout(resolve, time)
})
}
setTimeout
is callback based, but now we can use the await
or .then
on it because we’ve
reached into it with a promise and this gives us a uniform API if we’d been working with promises
all along. Thanks to await
we can have a sleep function without the hassles of doing the work we
want done after sleeping in another code block or a callback. Just right after the sleep we can
write the next chore to be done by JavaScript.
Working with multiple promises
Sometimes, we need to work with multiple promises and most times we may not be concerned by the fulfillment of individual promises, but their overall fulfillment or at least the fulfilment of anyone of them. Here is where some static promise methods come in.
Promise.all
Promise.allSettled
Promise.any
Promise.race
Other static methods are Promise.resolve
and Promise.reject
but their use case is different from
the above ones
Promise.all
Returns a single promise that resolves to an array of respective values when all promises resolves
and rejects when at least one of the promises rejects. Promise.all
works on the all-or-nothing
principle.
function wait(time) {
return new Promise(resolve => {
setTimeout(() => resolve(time), time)
})
}
function wait2(time) {
return new Promise((_r, reject) => {
setTimeout(() => reject(new Error('Rejected')), time)
})
}
async function exec() {
// All promises resolves/fulfills
// resolves to [4000, 2000]
let p1 = await Promise.all([wait(4000), wait(2000)])
// The promise that settles first is a rejected promise
// The promise that settles last is a fulfilled/resolved promise
// rejects with an Error without waiting for the last settled
let p2 = await Promise.all([wait(4000), wait2(2000)])
// The promise that settles first is a fulfilled/resolved promise
// The promise that settles last is a rejected promise
// rejects with an Error
let p3 = await Promise.all([wait2(4000), wait(2000)])
// All promises rejected
// rejects with an Error
let p4 = await Promise.all([wait2(4000), wait2(2000)])
}
Promise.allSettled
Returns a single promise that resolves to an array of objects that contains the state of all settled (resolved or rejected) promises.
interface Fulfilled {
status: 'fulfilled'
value: any
}
interface Rejected {
status: 'rejected'
resaon: any
}
async function exec() {
let p1 = await Promise.allSettled([wait(4000), wait(2000)]) // resolves to [Fulfilled, Fulfilled]
let p2 = await Promise.allSettled([wait(4000), wait2(2000)]) // resolves to [Fulfilled, Rejected]
let p3 = await Promise.allSettled([wait2(4000), wait(2000)]) // resolves to [Rejected, Fulfilled]
let p4 = await Promise.allSettled([wait2(4000), wait2(2000)]) // resolves to [Rejected, Rejected]
}
Promise.any
Returns a single promise that resolves to the first promise that resolves/fulfills and rejects with
an AggregationError
only when all promises rejects.
async function exec() {
// All promises resolves/fulfills
// resolves to 2000 without waiting for the last settled
let p1 = await Promise.any([wait(4000), wait(2000)])
// The promise that settles first is a rejected promise
// The promise that settles last is a fulfilled/resolved promise
// resolves to 4000 after the first settled rejects
let p2 = await Promise.any([wait(4000), wait2(2000)])
// The promise that settles first is a fulfilled/resolved promise
// The promise that settles last is a rejected promise
// resolves to 2000 without waiting for the last settled
let p3 = await Promise.any([wait2(4000), wait(2000)])
// All promises rejected
// rejects with an AggregationError of both promises rejection
let p4 = await Promise.any([wait2(4000), wait2(2000)])
}
Promise.race
Returns a single promise that resolves to or rejects with the first settled (resolved or rejected, respectively) promise.
async function exec() {
// All promises resolves/fulfills
// resolves to 2000 without waiting for the last settled
let p1 = await Promise.race([wait(4000), wait(2000)])
// The promise that settles first is a rejected promise
// The promise that settles last is a fulfilled/resolved promise
// rejects with an Error when the first settled rejects
let p2 = await Promise.race([wait(4000), wait2(2000)])
// The promise that settles first is a fulfilled/resolved promise
// The promise that settles last is a rejected promise
// resolves to 2000 when the first settled resolves without waiting for the last settled
let p3 = await Promise.race([wait2(4000), wait(2000)])
// All promises rejected
// rejects with an Error when the first settled rejects without waiting for the last settled
let p4 = await Promise.race([wait2(4000), wait2(2000)])
}
Promise.any
and Promise.race
may seem like they work the same but they are different—take a
closer look. I promise you’ll notice the difference.