Developer's guide to javascript promises
A Promise is a class that executes a callback and allows you to trigger other promises when the callback completes or fails.
1function promiseTimeout(delay) {2 return new Promise((resolve) => {3 setTimeout(() => resolve(), delay)4 }).then(() => {5 setTitle('Async, Await, and Promises')6 })7}89promiseTimeout(1000)10There's a lot going on here, so we'll break it down from the inside out
First, setTimeout waits until the delay is up, then triggers the callback by running the Promise's resolve() function
The callback to a Promise is defined by chaining a method called .then(callback)
Right now it seems like it's just a more complicated way of writing callbacks, but the advantage comes in when you want to refactor
1function promiseTimeout(delay) {2 return new Promise((resolve) => {3 setTimeout(() => resolve(), delay)4 })5}67promiseTimeout(1000).then(() => setTitle('Async, Await, and Promises'))8The .then() method always returns a promise. If you try to return a regular value, it will return a promise that resolves instantly to that value
Since it returns a promise, you can chain .then() onto the result indefinitely
So either of these patterns are valid
1promiseTimeout(1000).then(() => {2 setTitle('Async, Await, and Promises')3 setTitle('Async, Await, and Promises')4 setTitle('Async, Await, and Promises')5})61promiseTimeout(1000)2 .then(() => setTitle('Async, Await, and Promises'))3 .then(() => setTitle('Async, Await, and Promises'))4 .then(() => setTitle('Async, Await, and Promises'))5If the callback passed to .then() is a promise, it will wait for the promise to resolve before executing the next .then()
1promiseTimeout(1000)2 .then(() => setTitle('One second'))3 .then(() => promiseTimeout(5000)4 .then(() => setTitle('Six total seconds'))5Constructor
One way to create a Promise is through the constructor. This is most useful when you're wrapping a function that uses non-promise callbacks.
1const promise = new Promise((resolve, reject) => {2 resolve(data) // Trigger .then(callback(data))3 reject(error) // Trigger .catch(callback(error))4})5To use a real-world example, Node.js has a method for loading files called readFileAsync that looks like this
1fs.readFileAsync('image.png', (error, data) => {})2If we want to turn that into a promise, we're going to have to wrap it in one.
1function getImage(index) {2 return new Promise((resolve, reject) => {3 fs.readFileAsync('image.png', (error, data) => {4 if (error) {5 reject(error)6 } else {7 resolve(data)8 }9 })10 })11}12Tip: Node.js includes a promisify method that transforms a function that uses traditional callbacks to one that returns a promise. The implementation is exactly like the example above.
Class Method
Another way to create a promise is use the static class methods
Promise.resolve('value') will return a resolved promise. It will immediately begin to execute the next .then() method it has, if any.
Promise.reject('error') will return a rejected promise. It will immediately begin to execute the next .catch() method it has, if any.
1function getProducts() {2 if (!isCacheExpired) {3 return Promise.resolve(getProductsFromCache())4 }56 // The built-in method fetch() returns a promise7 return fetch('api/products')8 .then((response) => response.json())9 .then((products) => {10 saveProductsToCache(products)1112 return products13 })14}15Imagine you're trying to download a list of products from an API. Since it doesn't change very often, and API requests can be expensive, you might want to only make API requests if the list you already have is more than a few minutes old.
First we check if the cache has expired, and if not we return a promise resolving to the products we've already saved to it.
Otherwise the products are out of date, so we return a promise that fetches them from the API, saves them to the cache, and resolves to them.
Catch
While .then() triggers when a previous promise resolves, .catch() triggers when a previous promise either rejects or throws an error.
If either of those happen, it will skip every .then() and execute the nearest .catch()
1fetch('api/products')2 .then((response) => response.json())3 .then((products) => {4 saveProductsToCache(products)56 return products7 })8 .catch(console.error)9If .catch() returns anything or throws another error, it will continue down the chain just like before
Async Functions
To make writing promises easier, ES7 brought us the async keyword for declaring functions
A function declared with the async keyword always returns a promise. The return value is wrapped in a promise if it isn't already one, and any errors thrown within the function will return a rejected promise.
Usage
This is how to use it in a function
1async function getProducts() { }23const getProducts = async function() => { }45const getProducts = async () => { }6And in a method:
1const products = {2 async get() {},3}4Return
Whenever an async function returns, it ensures its return value is wrapped in a promise.
1async function getProducts() {2 return [3 {id: 1, code: 'TOOL', name: 'Shiny Hammer'},4 {id: 2, code: 'TOOL', name: 'Metal Corkscrew'},5 {id: 3, code: 'TOOL', name: 'Rusty Screwdriver'},6 {id: 1, code: 'FOOD', name: 'Creamy Eggs'},7 {id: 2, code: 'FOOD', name: 'Salty Ham'},8 ]9}1011getProducts().then((products) => {12 console.log(products)13 // Array (5) [ {…}, {…}, {…}, {…}, {…} ]14})15Throw
If an async function throws an error, it returns a rejected promise instead. This can be caught with the promise.catch() method instead of wrapping the function in try/catch statements
1async function failInstantly() {2 throw new Error('oh no')3}45failInstantly().catch((error) => {6 console.log(error.message)7 // 'oh no'8})9In a regular function, you need to catch errors using the classic try/catch statement syntax
1function failInstantly() {2 throw new Error('oh no')3}45try {6 failInstantly()7} catch (error) {8 console.log(error.message)9 // 'oh no'10}11Await
The other difference between regular functions and async functions is that async functions allow the use of the await keyword inside.
Await works like the .then() method, but instead of being a chained callback, it pulls the value out of the promise entirely.
Consider the previous example
1getProducts().then((products) => {2 console.log(products)3 // Array (5) [ {…}, {…}, {…}, {…}, {…} ]4})5And the same with await
1const products = await getProducts()23console.log(products)4// Array (5) [ {…}, {…}, {…}, {…}, {…} ]5It's important to remember that since await can only be used inside async functions (which always return a promise) you can't use this to pull asynchronous data out into synchronous code. In order to use await on a promise you must be inside another promise.