Today, we're going to look at what we need to know to understand Promises from a high-level, so we can build our applications using this incredibly useful concept.
Yesterday
we installed the fetch
library into our
create-react-app
project we started on
day 12. Today we'll pick up from yesterday discussing the
concept and the art of
Promises.
What is a promise
As defined by the Mozilla, a Promise
object is
used for handling asynchronous computations which has some
important guarantees that are difficult to handle with the
callback method (the more old-school method of handling
asynchronous code).
A Promise
object is simply a wrapper around a
value that may or may not be known when the object is
instantiated and provides a method for handling the value
after it is known (also known as
resolved
) or is unavailable for a failure reason
(we'll refer to this as rejected
).
Using a Promise
object gives us the opportunity
to associate functionality for an asynchronous
operation's eventual success or failure (for whatever
reason). It also allows us to treat these complex scenarios by
using synchronous-like code.
For instance, consider the following synchronous code where we print out the current time in the JavaScript console:
var currentTime = new Date();
console.log('The current time is: ' + currentTime);
This is pretty straight-forward and works as the
new Date()
object represents the time the browser
knows about. Now consider that we're using a different
clock on some other remote machine. For instance, if
we're making a Happy New Years clock, it would be great
to be able to synchronize the user's browser with
everyone elses using a single time value for everyone so
no-one misses the ball dropping ceremony.
Suppose we have a method that handles getting the current time
for the clock called getCurrentTime()
that
fetches the current time from a remote server. We'll
represent this now with a setTimeout()
that
returns the time (like it's making a request to a slow
API):
function getCurrentTime() {
// Get the current 'global' time from an API
return setTimeout(function() {
return new Date();
}, 2000);
}
var currentTime = getCurrentTime()
console.log('The current time is: ' + currentTime);
Our console.log()
log value will return the
timeout handler id, which is definitely not the
current time. Traditionally, we can update the code using a
callback to get called when the time is available:
function getCurrentTime(callback) {
// Get the current 'global' time from an API
return setTimeout(function() {
var currentTime = new Date();
callback(currentTime);
}, 2000);
}
getCurrentTime(function(currentTime) {
console.log('The current time is: ' + currentTime);
});
What if there is an error with the rest? How do we catch the error and define a retry or error state?
function getCurrentTime(onSuccess, onFail) {
// Get the current 'global' time from an API
return setTimeout(function() {
// randomly decide if the date is retrieved or not
var didSucceed = Math.random() >= 0.5;
if (didSucceed) {
var currentTime = new Date();
onSuccess(currentTime);
} else {
onFail('Unknown error');
}
}, 2000);
}
getCurrentTime(function(currentTime) {
console.log('The current time is: ' + currentTime);
}, function(error) {
console.log('There was an error fetching the time');
});
Now, what if we want to make a request based upon the first
request's value? As a short example, let's reuse the
getCurrentTime()
function inside again (as though
it were a second method, but allows us to avoid adding another
complex-looking function):
function getCurrentTime(onSuccess, onFail) {
// Get the current 'global' time from an API
return setTimeout(function() {
// randomly decide if the date is retrieved or not
var didSucceed = Math.random() >= 0.5;
console.log(didSucceed);
if (didSucceed) {
var currentTime = new Date();
onSuccess(currentTime);
} else {
onFail('Unknown error');
}
}, 2000);
}
getCurrentTime(function(currentTime) {
getCurrentTime(function(newCurrentTime) {
console.log('The real current time is: ' + currentTime);
}, function(nestedError) {
console.log('There was an error fetching the second time');
})
}, function(error) {
console.log('There was an error fetching the time');
});
Dealing with asynchronousity in this way can get complex quickly. In addition, we could be fetching values from a previous function call, what if we only want to get one... there are a lot of tricky cases to deal with when dealing with values that are not yet available when our app starts.
Enter Promises
Using promises, on the other hand helps us avoid a lot of this complexity (although is not a silver bullet solution). The previous code, which could be called spaghetti code can be turned into a neater, more synchronous-looking version:
function getCurrentTime() {
// Get the current 'global' time from an API using Promise
return new Promise((resolve, reject) => {
setTimeout(function() {
var didSucceed = Math.random() >= 0.5;
didSucceed ? resolve(new Date()) : reject('Error');
}, 2000);
})
}
getCurrentTime()
.then(currentTime => getCurrentTime())
.then(currentTime => {
console.log('The current time is: ' + currentTime);
return true;
})
.catch(err => console.log('There was an error:' + err))
This previous source example is a bit cleaner and clear as to what's going on and avoids a lot of tricky error handling/catching.
To catch the value on success, we'll use the
then()
function available on the
Promise
instance object. The
then()
function is called with whatever the
return value is of the promise itself. For instance, in the
example above, the getCurrentTime()
function
resolves with the currentTime()
value (on
successful completion) and calls the
then()
function on the return value (which is
another promise) and so on and so forth.
To catch an error that occurs anywhere in the promise chain,
we can use the catch()
method.
We're using a promise chain in the above example to create a chain of actions to be called one after another. A promise chain sounds complex, but it's fundamentally simple. Essentially, we can "synchronize" a call to multiple asynchronous operations in succession. Each call to
then()
is called with the previousthen()
function's return value.For instance, if we wanted to manipulate the value of the
getCurrentTime()
call, we can add a link in the chain, like so:getCurrentTime() .then(currentTime => getCurrentTime()) .then(currentTime => { return 'It is now: ' + currentTime; }) // this logs: "It is now: [current time]" .then(currentTimeMessage => console.log(currentTimeMessage)) .catch(err => console.log('There was an error:' + err))
Single-use guarantee
A promise only ever has one of three states at any given time:
- pending
- fulfilled (resolved)
- rejected (error)
A pending promise can only ever lead to either a fulfilled state or a rejected state once and only once, which can avoid some pretty complex error scenarios. This means that we can only ever return a promise once. If we want to rerun a function that uses promises, we need to create a new one.
Creating a promise
We can create new promises (as the example shows above) using
the Promise
constructor. It accepts a function
that will get run with two parameters:
-
The
onSuccess
(orresolve
) function to be called on success resolution -
The
onFail
(orreject
) function to be called on failure rejection
Recalling our function from above, we can see that we call the
resolve()
function if the request succeeded and
call the reject()
function if the method returns
an error condition.
var promise = new Promise(function(resolve, reject) {
// call resolve if the method succeeds
resolve(true);
})
promise.then(bool => console.log('Bool is true'))
Now that we know what promises are, how to use, and how to
create them, we can actually get down to using the
fetch()
library we installed yesterday. dd