Subscribe to my email list now at http://jauyeung.net/subscribe/
Follow me on Twitter at https://twitter.com/AuMayeung
Many more articles at https://medium.com/@hohanga
Because JavaScript is a single-threaded language, synchronous code can only be executed one line at a time. This means that if there’s synchronous code that runs longer than instantaneously, then it will stop the rest of the code from running until whatever is being run is finished. To prevent code that runs for an indeterminate amount of time from holding up other code from running, we need to have asynchronous code.
Promises
To do this in JavaScript, we can use promises in our code. Promises are objects that represent a process that runs for an indeterminate amount of time and may result in success or failure. To create promises in JavaScript, we use a Promise
object that’s a constructor for creating promises. The Promise
constructor takes a function called the executor function that has the resolve
and reject
parameters. Both parameters are functions that we call to either fulfill the promise (which means end it successfully with some value) or to reject the promise (which means returning some error value and marking the promise as failed). The function’s return value is ignored. Therefore, promises can’t return anything other than promises.
For example, we can define a promise in JavaScript like in the following code:
const promise = new Promise((resolve, reject) => {
setTimeout(() => resolve('abc'), 1000);
});
The code above creates a promise that fulfills the promise with the value abc
after one second. Because we’re running setTimeout
inside the executor function to resolve the promise with the value abc
in one second, it’s asynchronous code. We can’t return the value abc
within the callback function of setTimeout
, so we have to call resolve('abc')
to get the resolved value back. We can access the resolved value of the fulfilled promise by using the then
function. The then
function takes a callback function that takes the resolved value of the fulfilled promise as a parameter. You can get the value there and do what you want with it. For example, we can do something like:
const promise = new Promise((resolve, reject) => {
setTimeout(() => resolve('abc'), 1000);
});
promise.then((val) => {
console.log(val);
})
When we run the code above, we should see abc
logged. As we can see, the promise will supply the value when the promise is fulfilled with a call to the resolve
function.
A promise has three states. It can be pending, which means that the promise has been fulfilled or rejected. It can be fulfilled, which means that the operation completed successfully. Or it can be rejected, which means that the promise operation failed.
A pending promise can either be fulfilled with a value or be rejected with some error. When a promise is fulfilled, then the corresponding resolve value is picked up by the then
function and the callback function passed into the then
function is called. If a promise is rejected, then we can optionally catch the error with the catch
function, which also takes a callback function with the error. Both the then
and catch
functions return promises, so they can be chained together.
For example, we can write something like:
const promise = (num) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (num === 1) {
resolve('resolved')
} else {
reject('rejected')
}
}, 1000);
});
}
promise(1)
.then((val) => {
console.log(val);
})
.catch((error) => {
console.log(error);
})promise(2)
.then((val) => {
console.log(val);
})
.catch((error) => {
console.log(error);
})
In the code above, we have a function promise
that returns a JavaScript promise that fulfills the promise with the value resolved
when num
is 1, and reject the promise with error rejected
otherwise. So we run:
promise(1)
.then((val) => {
console.log(val);
})
.catch((error) => {
console.log(error);
})
Then the then
function runs, the promise returned by the promise(1)
function call is fulfilled since num
is 1, and the resolved value is available set in val
. So when we run console.log(val)
we get resolved
logged. When we run the code below:
promise(2)
.then((val) => {
console.log(val);
})
.catch((error) => {
console.log(error);
})
Then the catch
function runs because the promise returned by the promise(2)
function call is rejected, and the rejected error value is available set in error. So when we run console.log(error)
we get rejected
logged.
A JavaScript promise object has the following properties: length
and prototype
. The length
always has its value set to 1 for the number of constructor arguments, which is always one. The prototype
property represents the prototype of the promise object.
A promise also has the finally
method to run code that’s run no matter whether the promise is fulfilled or rejected. The finally
method takes a callback function as an argument, where any code that you want to run, regardless of the outcome of the promise, is run. For example, if we run:
Promise.reject('error')
.then((value) => {
console.log(value);
})
.catch((error) => {
console.log(error);
})
.finally(() => {
console.log('finally runs');
})
Then we get error
and finally runs
logged because the original promise was rejected with the reason error
. Then whatever code in the callback of the finally
method runs.
The main benefit of using promises for writing asynchronous code is that we can use them to run asynchronous code sequentially. To do this, we can chain promises with the then
function. The then
function takes a callback function that runs when the promise is fulfilled. It also takes a second argument when the promise is rejected. To chain promises, we have to return another promise as the return value of the first callback function of the then
function. It can return other values, like nothing, if we don’t want to chain another promise to the existing promise. We can return a value, which will be resolved and the value retrievable in the next then
function. It can also throw an error. Then the promise returned by then
gets rejected with the thrown error as the value. It can also return an already fulfilled or rejected promise, which will get the fulfilled value when a then
function is chained after it or get an error reason in the callback of the catch
function.
For example, we can write the following:
Promise.resolve(1)
.then(val => {
console.log(val);
return Promise.resolve(2)
})
.then(val => {
console.log(val);
})Promise.resolve(1)
.then(val => {
console.log(val);
return Promise.reject('error')
})
.then(val => {
console.log(val);
})
.catch(error => console.log(error));Promise.resolve(1)
.then(val => {
console.log(val);
throw new Error('error');
})
.then(val => {
console.log(val);
})
.catch(error => console.log(error));
In the first example, we chained the promises and they all resolve to a value. All the promises were already resolved into values. In the second and last examples, we reject the second promise or throw an error. They both do the same thing. The second promise is rejected, and the error reason will be logged in the callback of the catch
function. We can also chain pending promises, like in the following code:
const promise1 = new Promise((resolve, reject) => {
setTimeout(() => resolve(1), 1000);
});
const promise2 = new Promise((resolve, reject) => {
setTimeout(() => resolve(2), 1000);
});
promise1
.then(val => {
console.log(val);
return promise2;
})
.then(val => {
console.log(val);
})
.catch(error => console.log(error));
The callback of the first then
function returned promise2
, which is a pending promise.
Methods
JavaScript promises have the following methods.
Promise.all(iterable)
Promise.all
takes an iterable object that lets us run multiple promises in parallel in some computers and serially in other computers. This is handy for running multiple promises that don’t depend on each other’s values. It takes in an iterable with a list of promises, usually an array, and then returns a single Promise
that’s resolved when the promises that are in the iterable are resolved.
For example, we can write code like the following to run multiple promises with Promise.all
:
const promise1 = Promise.resolve(1);
const promise2 = 2;
const promise3 = new Promise((resolve, reject) => {
setTimeout(() => resolve(3), 1000);
});
Promise.all([promise1, promise2, promise3])
.then((values) => {
console.log(values);
});
If we run the code above, then the console.log
should log [1,2,3]
. As we can see, it only returns a resolved value after all the promises are fulfilled. If some of them are rejected, then we won’t get any resolved value. Instead, we will get whatever error value is returned by the rejected promises. It will stop at the first rejected promise and send that value to the callback of the catch
function. For example, if we have:
const promise1 = Promise.resolve(1);
const promise2 = Promise.reject(2);
const promise3 = new Promise((resolve, reject) => {
setTimeout(() => reject(3), 1000);
});
Promise.all([promise1, promise2, promise3])
.then((values) => {
console.log(values);
})
.catch(error => {
console.log(error);
});
Then we get 2 logged from the console.log
in the callback of the catch
function.
Promise.allSettled
Promise.allSettled
returns a promise that resolves after all the given promises are resolved or rejected. It takes an iterable object with a collection of promises, for example, an array of promises. The resolved value of the returned promise is an array of the final status of each promise. For example, suppose we have:
const promise1 = Promise.resolve(1);
const promise2 = Promise.reject(2);
const promise3 = new Promise((resolve, reject) => {
setTimeout(() => reject(3), 1000);
});
Promise.allSettled([promise1, promise2, promise3])
.then((values) => {
console.log(values);
})
If we run the code above, then we get an array with three entries, with each entry being an object that has the status
and value
properties for the fulfilled promises and an object that has the status
and reason
properties for the rejected promises. For example, the code above will log {status: “fulfilled”, value: 1}
, {status: “rejected”, reason: 2}
, {status: “rejected”, reason: 3}
. The fulfilled
status is logged for the successful promises and therejected
status for the rejected ones.
Promise.race
The Promise.race
method returns a promise that resolves to the resolved value of the promise that’s fulfilled first. It takes an iterable object with a collection of promises, for example, an array of promises. If the iterable passed in is empty, then the returned promise will forever be pending. If the iterable object contains one or more non-promise values or already settled promises, then Promise.race
will return to the first of these entries. For example, if we have:
const promise1 = Promise.resolve(1);
const promise2 = Promise.resolve(2);
const promise3 = new Promise((resolve, reject) => {
setTimeout(() => resolve(3), 1000);
});
Promise.race([promise1, promise2, promise3])
.then((values) => {
console.log(values);
})
Then we see that 1
is logged. That’s because promise1
is the first one that’s resolved since it’s resolved instantaneously before the next lines are run. Likewise, if we have a non-promise value in the array that we pass in as the argument, like in the following code:
const promise1 = 1;
const promise2 = Promise.resolve(2);
const promise3 = new Promise((resolve, reject) => {
setTimeout(() => resolve(3), 1000);
});
Promise.race([promise1, promise2, promise3])
.then((values) => {
console.log(values);
})
Then we get the same thing logged since it’s the non-promise value in the array that we pass into the Promise.race
method. Synchronous code always runs before the asynchronous code, so it doesn’t matter where the synchronous code is. If we have:
const promise1 = new Promise((resolve, reject) => {
setTimeout(() => resolve(1), 2000);
});
const promise2 = new Promise((resolve, reject) => {
setTimeout(() => resolve(2), 1000);
});
const promise3 = 3;
Promise.race([promise1, promise2, promise3])
.then((values) => {
console.log(values);
})
Then we get 3
logged since setTimeout
puts the callback function in the queue to be run later, so it’s going to be run later than the synchronous code.
Finally, if we have:
const promise1 = new Promise((resolve, reject) => {
setTimeout(() => resolve(1), 2000);
});
const promise2 = new Promise((resolve, reject) => {
setTimeout(() => resolve(2), 1000);
});
Promise.race([promise1, promise2])
.then((values) => {
console.log(values);
})
Then we get 2
in the console log because a promise that’s resolved in one second is going to be resolved earlier than a promise resolved in two seconds.
Promise.reject
Promise.reject
returns a promise that’s rejected with a reason. It’s useful to reject a promise with an object that’s an instance of Error
. For example, if we have the following code:
Promise.reject(new Error('rejected'))
.then((value) => {
console.log(value);
})
.catch((error) => {
console.log(error);
})
Then we get rejected
logged.
Promise.resolve
Promise.resolve
returns a promise that’s resolved to the value that’s passed into the argument of the resolve
function. We can also pass in an object with the then
property where the value of it is the callback function of a promise. If the value has a then
method, then the promise will be fulfilled with the value fulfilled by the then
function. That is, the first parameter of the function for the value of the then
function is the same as resolve
, and the second parameter is the same as reject.
For example, we can write the following:
Promise.resolve(1)
.then((value) => {
console.log(value);
})
Then we get 1
logged since 1
is the value we passed into the resolve
function to return the promise with resolved value 1
.
If we pass in an object with a then
method inside, like in the following code:
Promise.resolve({
then(resolve, reject) {
resolve(1);
}
})
.then((value) => {
console.log(value);
})
Then we get value 1
logged. That’s because the Promise.resolve
function will run the then
function, and the resolve
parameter for the function set to the then
property will be assumed to be a function that’s called the resolve
function in a promise. If we instead called the reject
function inside the then
function in the passed-in object, then we get a rejected promise, like in the following code:
Promise.resolve({
then(resolve, reject) {
reject('error');
}
})
.then((value) => {
console.log(value);
})
.catch((error) => {
console.log(error);
})
In the code above, we get error
logged since the promise is rejected.
Async and Await
With async
and await
, we can shorten the promise code. Before async
and await
, we had to use the then
function and we had to put callback functions as an argument of all of our then
functions. This makes the code long as we have lots of promises. Instead, we can use the async
and await
syntax to replace the then
function and its associated callbacks. For example, we can shorten the following code from:
const promise1 = new Promise((resolve, reject) => {
setTimeout(() => resolve(1), 2000);
});
const promise2 = new Promise((resolve, reject) => {
setTimeout(() => resolve(2), 1000);
});
promise1
.then((val1) => {
console.log(val1);
return promise2;
})
.then((val2) => {
console.log(val2);
})
to:
const promise1 = new Promise((resolve, reject) => {
setTimeout(() => resolve(1), 2000);
});
const promise2 = new Promise((resolve, reject) => {
setTimeout(() => resolve(2), 1000);
});
(async () => {
const val1 = await promise1;
console.log(val1)
const val2 = await promise2;
console.log(val2)
})()
We replaced the then
and callbacks with await
. Then we can assign the resolved values of each promise as variables. Note that if we use await
for our promise code, then we have to put async
in the function signature as we did in the above example. To catch errors, instead of chaining the catch
function at the end, we use the catch
clause. Also, instead of chaining the finally
function at the bottom to run code when a promise ends, we use the finally
clause after the catch
clause.
For example, we can write:
const promise1 = new Promise((resolve, reject) => {
setTimeout(() => resolve(1), 2000);
});
const promise2 = new Promise((resolve, reject) => {
setTimeout(() => reject('error'), 1000);
});
(async () => {
try {
const val1 = await promise1;
console.log(val1)
const val2 = await promise2;
console.log(val2)
} catch (error) {
console.log(error)
} finally {
console.log('finally runs');
}})()
In the code above, we got the resolved value of the promise assigned to a variable instead of getting the value in the callback of the then
function, like in the const response = await promise1
line above. Also, we used the try...catch...finally
blocks to catch the errors of rejected promises and the finally
clause instead of the finally
function with a callback passed in to create code that runs no matter what happens to the promises.
Like any other function that uses promises, async
functions always return promises and cannot return anything else. In the example above, we showed that we can chain promises in a much shorter way than with the then
function with callbacks passed in as an argument.
Conclusion
With promises, we can write asynchronous code with ease. Promises are objects that represent a process that runs for an indeterminate amount of time and may result in success or failure. To create promises in JavaScript, we use a Promise
object that’s a constructor for creating promises.
The Promise
constructor takes a function called the executor function that has the resolve
and reject
parameters. Both parameters are functions that we call to either fulfill the promise (which means end it successfully with some value) or to reject the promise (which means returning some error value and marking the promise as failed). The function’s return value is ignored. Therefore, promises can’t return anything other than promises.
Promises are chainable since promises return promises. The then
function of promises can throw an error, return resolved values, or return other promises that are pending, fulfilled, or rejected.
Top comments (4)
Great article! Explains promises easily and exhaustively.
Thanks so much for reading.
Thank you , very exhaustive
Thanks so much for reading.