Oh My Promises
I’ve been shamingly using Promises for years without really taking the time to explore beyond then
and catch
.
Big big mistake. There’s so much more. then
and catch
are just the tip of the iceberg.
Promise, the main idea
Let’s begin considering what’s a Promise
.
Loosely speaking, think of a Promise
as an object which may deliver a value.
As the name Promise
suggests, the concept behind is: I don’t have a value now but I promise to deliver once I have it.
As such, a Promise
can be in one of these states:
pending
: initial state, on waiting.fulfilled
: operation resolved, successfully completed (on success).rejected
: operation rejected, failed (on failed).
When the Promise
is either fulfilled
or rejected
can be called settled
.
Note that settled
is not a state per se, but just a convenient term you can use when you want to say a Promise
is completed.
At this point you may feel the harshness of the definition, so let’s use an analogy to clear things up.
Imagine a friend who wants to borrow some money from you. If you agree, your friend promises to repay the money in 5 days.
Assuming you lend the money, you’ll be in the pending
state (on waiting) for 5 days.
After 5 days, two things could happen. One, your friend returns the money, in that case, the return money operation is successfully resolved (fulfilled
). Two, something happens and your friend is not able to return the money, the return money operation failed (rejected
).
In pseudo-code it could look like this:
var returnMoneyPromiseA =
new Promise((resolve, reject) => {
setTimeout(() => {
resolve('event on success, friend return money')
}, 5_DAYS_IN_MILLISEC);
});
// When returnMoneyPromiseA is created is in pending state
// Promise {<pending>}
// this is because your friend will return the money in 5 days (setTimout)
// for the next 5 days returnMoneyPromiseA is in state `pending`
// After 5 days returnMoneyPromiseA will be
// Promise {<fulfilled>: 'event on success, friend return money'}
var returnMoneyPromiseB =
new Promise((resolve, reject) => {
setTimeout(() => {
reject('event on failure, friend not able to return money')
}, 5_DAYS_IN_MILLISEC);
});
// When returnMoneyPromiseB is created is in pending state
// Promise {<pending>}
// this is because your friend should return the money in 5 days
// for the next 5 days returnMoneyPromiseB is in state `pending`
// Your friend couldn't give you the money back.
// After 5 days returnMoneyPromiseB will be
// Promise {<rejected>: 'event on failure, friend not able to return money'}
then
Let’s talk now about the most used method within promises: the then
method.
By itself then
is just a method that accepts two arguments:
- the function to call on fulfilment (equivalent of on success) and
- the function to call on reject (equivalent of on failure).
// Thenable interface
// Any object that has a then method.
then(onFulfilment, onReject) : PromiseLike
then(onSuccess, onFailure) : PromiseLike
The key point to remember is that then
always return another thenable object, that is a Promise
.
This is the most important™ and underrated detail about promises so I’ll repeat, then
always return another Promise
. Even when your onFulfilment
returns a number
, that will be cast to a fulfilled
Promise
.
This is powerful because you can keep passing the value to the next then
and so you can use Promise
to chain multiple functions till you get the desired result.
Note: The example uses the arrow function implicit return. If you need to refresh your memory about it read Arrow function expressions: Function body
// Pseudo code implements thenable interface
// See how a cast to promise could work
class Thenable {
constructor(value) {
this.value = value;
}
then(onFulfilment, onReject) {
const nextVal = onFulfilment(this.value);
return new Thenable(nextVal);
}
}
// Example of chaining functions
(new Thenable(6))
.then((val) => val + 5) // arrow function implicit return, returns 11
.then((val) => console.log(val)) // print in console 11
// Promise propagation and function chaining example
var p0 = new Promise((resolve) => resolve(5))
Promise{<fulfilled>: 5}
var p1 = p0.then(a => a + 1)
Promise{<fulfilled>: 6}
var p2 = p1.then(b => b + 2)
Promise{<fulfilled>: 8}
// all together
(new Promise((resolve) => resolve(5)))
.then(a => a + 1)
.then(b => b + 2)
.then(console.log) // print in console 8
In the first example, you can see how the value coming from onFulfilment
gets cast in a Thenable
, the same happens in the second example where the returned value gets cast to a fulfilled
Promise
.
Catch
Now that we saw how the then
method works we can start with the catch
method.
catch
is very similar to then
, so similar that it’s nothing more than a then
method with only the onReject
function.
// Equivalent syntax
catch(onReject)
then(undefined, onReject)
As then
, catch
will always return another fulfilled
Promise
.
An interesting case with catch
is when you chain multiple then
and one of them throw an error as the error stops the chain propagation and triggers the first catch
.
This is convenient as it allows avoiding unnecessary operations.
But before seeing a catch
example, let’s talk for a moment about Promise.resolve
as it will help with the explanation.
Promise.resolve
returns a fulfilled
Promise
which you can use to start the chain of then
. This is useful when you have a normal value but you still want to use the chains of then
.
Technically speaking, Promise.resolve
resolves a value to a fulfilled
Promise
.
// Equivalent Syntax
// no setTimeout, no delay
var A = Promise.resolve(5);
var B = new Promise((resolve, reject) => {
resolve(5)
});
// Promise {<fulfilled>: 5}
With Promise.resolve
in our toolbelt, let’s see a more interesting example.
// Let's assume we're receiving some user input.
// For example it could be a postcode and we want
// to perform some validation
const toString = (value) => '' + value;
const trim = (value) => value.trim();
const isNotEmpty = (value) => {
if (value.length === 0) {
throw Error('Postcode can not be empty');
}
return value;
};
const is4DigitLong = (value) => {
if (value.length !== 4) {
throw Error('Postcode must be 4 digits long');
}
return value;
}
Promise.resolve(' a ') // "resolves" text to a Promise
.then(toString) // force cast to string
.then(trim) // trim
.then(isNotEmpty) // pass check
.then(is4DigitLong) // throw error 'Postcode must be 4 digits long'
.catch(console.log) // print in console 'Postcode must be 4 digits long'
Promise.resolve(' ') // "resolves" text to a Promise
.then(toString) // force cast to string
.then(trim) // trim
.then(isNotEmpty) // throw error 'Postcode can not be empty'
.then(is4DigitLong) // - skipped because the previous error
.catch(console.log) // print in console 'Postcode can not be empty'
In the example, we created an input validator merely using Promise.resolve
, catch
and the chain of then
.
This should give you a sense of how easy and convenient it is to split and chain functions using then
and catch
.
What makes it so easy and convenient? Well, because all the functions within the then
are pure functions with no side effects, no global states, just plain input/output.
Aggregators
As you start dealing with group of promises there may be times when you may want to do something if the group is in a specific state.
In this type of situation aggregators, a set of Promise static methods, are here to help you.
To get an idea about how they work let’s examine Promise.all
.
Promise.all
allows you to get a group of promises and wait until all of them are successfully completed.
Promise.all
by itself returns a new Promise
and if any of the promises in the group fails then Promise.all
fails too.
So, let’s extend the input example above to a form example
// Let's assume you have a form with multiple inputs, each one with some validation
// in case of a validation fail you want to disable the submit button
function validatePostcode(value) {
return Promise.resolve(value)
.then(toString)
.then(trim)
.then(isNotEmpty)
.then(is4DigitLong)
.catch(() => (showErrorMessage(), Promise.reject("postcode-error")));
// Note how we use `Promise.reject` to propagate the
// rejection to `Promise.all` as by itself catch would
// return a fulfilled Promise, which would not trigger
// the catch at the `Promise.all` level.
}
// You can imagine several functions in the same spirit of validatePostcode
Promise.all([
validateName(inputName.value),
validateLastName(inputLastName.value),
validateStreet(inputStreet.value),
validatePostcode(inputStreet.value),
])
.then(enableSubmitButton) // if all the promises are fulfilled
.catch(disableSubmitButton); // if any promise is rejected
As you can see, you can use Promise.all
to create abstractions on top of group of promises which you can use to trigger actions.
The beauty of this is that you’re still using promises.
Summary
We’re close to the end so it’s time to recap.
We saw the general concept of a Promise
, the states it can be in, the then
method, catch
, Promise.resolve()
and Promise.all()
.
With these concepts in mind, you should be able to explore the docs. Probably the most interesting parts are going to be Promise.prototype.finally()
and the rest of the aggregator functions like Promise.allSettled
, Promise.race
and Promise.any
.
If you liked what you saw here it may interest you RxJS as well, which is basically Promises
with steroids. If the idea tingled your curiosity you can check RxJS in less than 5 mins.
Links
- Promises/A+
- States and fates
- Promise
- Promise.prototype.then()
- Promise.prototype.catch()
- Promise.resolve()
- Promise.all()
- Arrow function expressions : Function body
- RxJS in less than 5 mins