The Ultimate Javscript Promises Ramp-Up
I recently joined a new team and had to ramp-up on my Javascript knowledge. Promises were a standard concept that I needed to learn about. After a bit of ramping-up, I have written this tutorial in hopes to share what I have learned, all in one curated post.
This tutorial is geared towards those who have done Javascript before, and want to learn the powerful and popular concept - Promises. Maybe you played with it before. Maybe you are new to it. In either case, the goal of this post is to teach you how useful it can be for you.
If you are looking to ramp-up on your knowledge with Javascript Promises, you are at the right place!!
The code here follows StandardJS Rules.
Topics
- Promise States
- Promise.prototype.then()
- Promise.prototype.catch()
- Promise.prototype.finally()
- Promise.reject()
- Promise.resolve()
- Create a new Promise
- Promise.race()
- Promise.all()
- Async Await
Promise states
Here are the 4 states of a Promise that you need to know:
Pending - The final value of the promise is not available yet.
Settled - The promise has either been fulfilled or rejected.
Fulfilled - Success! This state is also known as resolved. Final value becomes available.
Rejected - An error has prevented the final value from being determined.
Promise.prototype.then()
.then(onFulfilled[, onRejected])
will take in the final value the promise returns.
Simple example
Here, we will make a simple fetch
call to the Reddit frontpage.
const redditPromise = fetch('https://www.reddit.com/.json')
redditPromise.then(value => {
console.log(`Success! We have the result: ${value.status}`) // Success! We have the result: 200
})
Return undefined
Here, we see that undefined values can be propagated.
redditPromise
.then(response => response.undefined_property)
.then(response => console.log(response)) // undefined
Chained promises with .then()
Here is a simple example that chains two .then()
calls. The return value of the first call propagates to the next.
redditPromise
.then(value => value.json())
.then(value => console.log(value.data.children.length)) // 25
Promise.prototype.catch()
Okay so we know how to use the then()
for fulfilled values, but what if an error is thrown? We could handle the error within the then()
block as shown:
const redditPromise = fetch('https://www.reAAAAAddit.com/.json') // Invalid endpoint
redditPromise.then(
response => {
// Fulfilled
console.log('Success') // Not printed.
},
error => {
// Rejected
console.error(error) // Printed.
})
That’s great, but what if an error is thrown from within the then()
scope?
const redditPromise = fetch('https://www.reddit.com/.json') // Valid endpoint
redditPromise.then(
response => {
console.log('Success') // Printed
throw Error() // Unhandled Promise Rejection
},
error => {
console.error(error) // Not printed
})
The error won’t be caught and you will have a crash. :(
Error handling with .then()
vs .catch()
Should you catch errors in the .then()
or in .catch()
? What’s the difference?
To understand this, let’s look at the following error flow.
.then()
is not responsible for errors that happen within its scope. It will just pass it on down the chain.
Let’s look at the difference with .catch()
.
Here, we see that errors thrown within the .then()
scope are caught!
So, here is the updated code.
redditPromise.then(
response => {
console.log('Success')
throw Error()
})
.catch(error => {
console.error(error) // Error is caught!
})
Promise.prototype.finally()
If you are familiar with try-catch-finally, then this one should be straight forward.
Here’s a question though, what’s the behavioral difference between using the following 2 patterns?
- try - catch - then
- try - catch - finally
Answer: if an error is thrown from the catch
, finally
in scenario 2 will run anyways, but then
in scenario 1 won’t unless it covers the error.
Scenario 1
const redditPromise = someBadFetch() // response.ok = false
redditPromise.then(
response => {
if (!response.ok) {
throw Error('unsuccessful response') // Throws
}
return response.json()
})
.catch(error => {
handleError(error)
throw Error() // Uh oh
})
.then(
() => {
cleanUp() // Not called
},
()) => {
cleanUp() // Called
}
)
Scenario 2
const redditPromise = someBadFetch() // response.ok = false
redditPromise.then(
response => {
if (!response.ok) {
throw Error('unsuccessful response') // Throws
}
return response.json()
})
.catch(error => {
handleError(error) // Called
})
.finally(() => {
cleanUp() // Called
})
.finally()
will always be called, regardless of whether or not .catch()
throws an error.
Promise.reject()
The opposite of .resolve()
.
You can create a Rejected Promise with .reject()
.
Promise.reject('nope')
.then(response => {
console.log('then') // Not printed
})
.catch(error => {
console.log(error) // Prints 'nope'
})
Normally, you would pass in an error to the reject, not a string.
Promise.reject(Error('nope'))
.then(response => {
...
})
.catch(error => {
...
})
Nesting promises will still return the final error.
Promise.reject(Error('nope'))
.then(response => {
...
})
.catch(error => {
return Promise.reject(Error('naw'))
})
.catch(error => {
handleError(error) // Error('naw')
})
Promise.resolve()
.resolve()
will return a promise that immedately fulfills with a value.
const resolvePromise = Promise.resolve(2000) // Promise that is immediatedly fulfilled with 2000.
resolvePromise.then(value => console.log(value)) // Prints 2000
Promise.resolve(resolvePromise) === resolvePromise // true
You might think you understand .resolve()
, but pay careful attention! There are some counter-intuitive things here.
Resolving a rejected promise
It doesn’t always return a fulfilled promise. Consider the following example where a promise tries to resolve a rejected promise.
const rejectedPromise = Promise.reject(Error('Bleh bleh'))
Promise.resolve(rejectedPromise)
.then(() => console.log('Fulfilled')) // Not called
.catch(() => console.error('Rejected')) // Called
Makes sense, right? But then consider the following example where the promise resolves an error.
Promise.resolve(Error)
.then(value => console.log(value)) // Prints out the error value
.catch(error => console.error(error)) // Not called
It’s fulfilled. This is because there is no special treatment for the Error
that is resolved. A Promise that is rejected will propagate towards the next error handler.
Convert a Thenable method into a native Promise with .resolve()
Let’s say you have a thenable method, like the following jQuery:
$.getJSON('https://www.reddit.com/.json')
// Result
// {readyState: 1, getResponseHeader: ƒ, getAllResponseHeaders: ƒ, setRequestHeader: ƒ, overrideMimeType: ƒ, …}
If you try to use the .finally()
, it won’t work. :(
$.getJSON('https://www.reddit.com/.json')
.then(value => ... )
.catch(err => ... )
.finally(() => ...)
// Uncaught TypeError: $.getJSON(...).then(...).catch is not a function
That’s because Promises came out a long time after jQuery, so you can’t really expect jQuery thenables to work like promises.
But, you can convert them into a native Promise with .resolve()
, like in the following:
Promise.resolve($.getJSON('https://www.reddit.com/.json') )
.then(result => doWork(result))
.catch(err => handleError(err))
.finally(() => cleanUp())
Create a new Promise
Creating a new Promise can be a useful way of encapsulating the workflow within promises.
Simple example
const promise = new Promise((resolve, reject) => {
// Do some stuff.
if (ok) {
resolve(result)
} else {
reject(error)
}
})
promise.then(
result => {
// Fulfilled
},
error => {
// Rejected
}
)
Scenario: Throwing an unexpected error
Can you guess what would happen in the following code?
const someBuggyPromise = new Promise(resolve => {
throw Error('Whoa')
resolve('Hello Promises')
})
Even if the promise doesn’t .reject()
anything, if an Error
is thrown, it will be rejected.
const someBuggyPromise = new Promise(resolve => {
throw Error('Whoa')
resolve('Hello Promises')
})
someBuggyPromise.then(
result => {
// Not called
},
error => {
// Called
}
)
Promise.race()
I haven’t had to use this one much, but I can see it being useful for setting timeouts on certain calls.
You can use .race()
to find the first promise that settles.
const promiseA = new Promise((resolve, reject) => {
setTimeout(resolve, 1000, '1')
})
const promiseB = new Promise((resolve, reject) => {
setTimeout(resolve, 10000, '2')
})
const fasterPromise = Promise.race([ promiseA, promiseB ])
fasterPromise.then(value => {
console.log(value) // Prints 1
})
Don’t forget to clear timers.
How long do you think the above code will take? promiseA
resolves after 1 second, so roughly around that, right?
Let’s run the following command to find out.
# Bash script
time node race.js
# Result
# real 0m10.319s
# user 0m0.015s
# sys 0m0.015s
Uh-oh. Our timeout for promiseB
with 10 seconds prevents Node.js from terminating the process.
For the sake of this example, let’s say we kept a timer Id for each timer.
let timerA, timerB
const promiseA = new Promise((resolve, reject) => {
timerA = setTimeout(resolve, 1000, '1')
})
const promiseB = new Promise((resolve, reject) => {
timerB = setTimeout(resolve, 10000, '2')
})
const fasterPromise = Promise.race([ promiseA, promiseB ])
fasterPromise.then(value => {
console.log(value) // Prints 1
})
.finally(()=> {
clearTimeout(timerA)
clearTimeout(timerB)
})
Let’s run the script again.
time node race.js
# Result
# real 0m1.333s
# user 0m0.000s
# sys 0m0.030s
BAM.
Promise.all()
.all()
takes in an iterable of promises, and returns a single promise that resolves when all promises that were passed in have resolved.
If one of the promises are rejected, then it will reject with the same error.
This is useful when you want to run async calls in parallel.
For the following example, we will try to get the latest price changes for Bitcoin and Ethereum. I have found them quite interesting, and want to know how the prices have changed in the past 24 hours. Let’s do that with promises.
function fetchCoinStats (coinId) {
return fetch(`https://api.coinlore.com/api/ticker/?id=${coinId}`)
.then(response => {
return response.ok
? response.json()
: Promise.reject(Error('Error with request'))
})
}
const bitcoinPromise = fetchCoinStats(90) // Bitcoin
const ethereumPromise = fetchCoinStats(80) // Ethereum
bitcoinPromise
.then(bitcoinStats => {
ethereumPromise.then(ethStats => {
const message = `Bitcoin price has changed ${bitcoinStats[0].percent_change_24h}% in the last day, while Ethereum has changed ${ethStats[0].percent_change_24h}%.`
displayOnUI(message)
})
})
.finally(() => {
cleanUpUI()
})
Depending on your network, you might get the wanted outcome where displayOnUI()
finishes first, then cleanUpUI()
is called after. But this may not always be the case. cleanUpUI()
could be called before displayOnUI
finishes.
This is because ethereumPromise
is a dangling promise. It’s not returned, so it never gets passed onto the promise chain.
Let’s change that.
bitcoinPromise
.then(bitcoinStats => {
return ethereumPromise.then(ethStats => {
const message = `Bitcoin price has changed ${bitcoinStats[0].percent_change_24h}% in the last day, while Ethereum has changed ${ethStats[0].percent_change_24h}%.`
displayOnUI(message)
})
})
.finally(() => {
cleanUpUI()
})
Now, cleanUpUI()
will always be called after both promises are finished.
But, it’s still not ideal. Right now, our promises are working like they are in a baton passing race. The call for ethereum starts after the call for bitcoin is fulfilled.
But they are both asynchronous calls, so it doesn’t have to be that way.Let’s make them happen in parallel with .all()
.
const promise = Promise.all([
bitcoinPromise,
ethereumPromise
])
promise.then(results => {
const bitcoinStats = results[0]
const ethStats = results[1]
const message = `Bitcoin price has changed ${bitcoinStats[0].percent_change_24h}% in the last day, while Ethereum has changed ${ethStats[0].percent_change_24h}%.`
displayUI(message)
})
.finally(() => {
cleanUpUI()
})
Try running the following code in your browser console.
function fetchCoinStats (coinId) {
return fetch(`https://api.coinlore.com/api/ticker/?id=${coinId}`)
.then(response => {
return response.ok
? response.json()
: Promise.reject(Error('Error with request'))
})
}
const bitcoinPromise = fetchCoinStats(90) // Bitcoin
const ethereumPromise = fetchCoinStats(80) // Ethereum
const promise = Promise.all([
bitcoinPromise,
ethereumPromise
])
promise.then(results => {
const bitcoinStats = results[0]
const ethStats = results[1]
const message = `Bitcoin price has changed ${bitcoinStats[0].percent_change_24h}% in the last day, while Ethereum has changed ${ethStats[0].percent_change_24h}%.`
console.log(message)
})
Async Await
I personally find that code with async
/ await
are easier to read than code with Promises. They are very useful, and they are still technically Promises. An easy way to go about it is to see async
/ await
as Promise wrappers.
Let’s go back to the previous example with Bitcoin.
Say I had the following code to get the latest Bitcoin price.
function getBitcoinStats() {
return fetchCoinStats(90).then(stats => {
console.log(stats[0].price_usd)
})
}
It returns a promise that will print the price of Bitcoin when fulfilled.
Let’s change it to an async await operation.
async function getBitcoinStats() {
const stats = await fetchCoinStats(90)
console.log(stats[0].price_usd)
}
getBitcoinStats() // Will fetch and print Bitcoin price
Explanation
async
is marking the function as asynchronous. This is done whenever there is anawait
call within the function.await
is making the program wait for the functionfetchCoinStats()
to be settled (remember the states?). It then returns the fulfilled value.
Question: what if an error is thrown from fetchCoinStats()
?
Answer: then the method getBitcoinStats()
will be equivalent to a rejected Promise that returns the error from fetchCoinStats()
.
async function getBitcoinStats() {
const stats = await fetchCoinStats(90) // Let's say this throws and error
console.log(stats[0].price_usd)
}
getBitcoinStats().catch(error => console.log(error)) // Catch the error
It works, but the code doesn’t look so good anymore. I need to look at multiple places to understand the workflow.
Instead, we can have try - catch - finally inside `getBitcoinStats()
async function getBitcoinStats() {
try {
const stats = await fetchCoinStats(90)
console.log(stats[0].price_usd)
} catch (error) {
handleError(error)
} finally {
cleanUp()
}
}
getBitcoinStats()
The code looks a lot more readable, and the workflow is all in one place! Great!
here’s another example. Let’s convert our fetchCoinStats()
method into async await.
Original
function fetchCoinStats (coinId) {
return fetch(`https://api.coinlore.com/api/ticker/?id=${coinId}`)
.then(response => {
return response.ok
? response.json()
: Promise.reject(Error('Error with request'))
})
}
Async Await
async function fetchCoinStats (coinId) {
const response = await fetch(`https://api.coinlore.com/api/ticker/?id=${coinId}`)
if (response.ok) {
return repsonse.json()
} else {
return Promise.reject(Error('Error with request'))
}
}
Finally, let’s combine with Promise.all() to make requests for other coins too.
You can try running the following code in your browser.
async function fetchCoinStats (coinId) {
const response = await fetch(`https://api.coinlore.com/api/ticker/?id=${coinId}`)
if (response.ok) {
return response.json()
} else {
return Promise.reject(Error('Error with request'))
}
}
async function getCryptoCoinStats() {
try {
const [bitcoin, ethereum, bitcoinCash] = await Promise.all([
fetchCoinStats(90), // Bitcoin
fetchCoinStats(80), // Ethereum
fetchCoinStats(2321) // Bitcoin cash
])
const message = `Bitcoin price has changed ${bitcoin[0].percent_change_24h}% in the last day, Ethereum has changed ${ethereum[0].percent_change_24h}%, and Bitcoin cash has changed ${bitcoinCash[0].percent_change_24h}%.`
console.log(message)
} catch (error) {
console.log(error)
} finally {
console.log('done')
}
}
getCryptoCoinStats()
That’s all!
Hope you learned something. Feel free to send me feedback.
Cheers and Happy Promises :)