Promises aren't just a way to deal with async operations...
Sebastien Filion
Posted on May 11, 2021
Oh, hey there!
So you think you understand Promises, huh?
In JavaScript, Promises are both a way to handle asynchronous operations and a data structure.
This article is a transcript of a Youtube video I made.
Whether you are making a HTTP request, querying a database or writing to the console, I/O operations can
be very slow. Because JavaScript is single-threaded by design -- can only do one thing at a time -- asynchronous
or async operations are very common.
Let me give you an example, say that when a user of a web app clicks on a button that trigger a HTTP request to
an API -- the JavaScript runtime had to wait for the request to resolve before handling any other operation,
it would make for a pretty sluggish experience.
Instead the engine makes the request, put it aside and gets ready to handle any other operations. Every now and then,
the process will look at the request -- and be like "are you done yet?". When the request finally resolves, the engine
will execute a function defined by the developer to handle the response.
You might know those as "callback functions".
A good example of this is setTimeout
. It's a function that takes another function as argument which will be
executed asynchronously later.
console.log("Before...");
setTimeout(() => console.log("...One second later"), 1000);
console.log("...After");
Callbacks works just fine in many cases but starts becoming especially difficult to deal with when multiple
inter-dependant async operations are needed.
retrieveCurrentUser((error, user) => {
if (error) return handleError(error);
setCurrentUserStatus(user.ID, "active", (error) => {
if (error) return handleError(error);
retriveActiveThreadsForUser(user.ID, 10, (error, threads) => {
if (error) return handleError(error);
threads.forEach(thread => subscribeToThread(thread.ID, user.ID, error => handleError(error)));
});
});
});
So let's talk about Promises. I mentioned earlier that a Promise is both a way to deal with async operations and a data
structure. Here's what I meant by that.
Imagine you have the number 42
that you assign to x
. From this point on, x
refers to the number 42
and can be
used as such. Imagine a function called f
that simply multiplies by 2 any number. Now, if we were to pass x
to the
function f
, it would produce a new number, 84
, that we can assign to the variable y
. From then on, y
is 84
.
const f = x => x * 2;
const x = 42;
const y = f(x);
A Promise represents a value that may or may not exists yet. If we assign p
as the Promise of 42
, you can also, say
that p
refers to the number 42
and be used as such. The difference is that because p
may or may not be 42
just
yet -- remember async operations -- so; the value, 42
, can't be accessed directly.
We use the .then
method to access and transform the value.
Similar to our previous example, if we have a function f
that multiplies any number by 2, and we apply it to our
Promise p
, it would produce a new Promise of the value 84
which we can assign to the variable q
. From then on, q
is a Promise of the number 84
. It is important to note that p
is still a Promise of 42
.
const f = x => x * 2;
const p = Promise.resolve(42);
const q = p.then(f);
So now, what if we have a function called g
that takes any number, multiplies it by 2, but returns a Promise of the
result? After we apply the function g
to our Promise p
-- which is still 42
, we still end up with a Promise of
84
.
const g = x => Promise.resolve(x * 2);
const r = p.then(g);
The rule is that if a function returns a value that is not a Promise, the value will be wrapped in a new Promise. But
if the value is already a Promise, we don't need to wrap it again!
A Promise represents a value that may or may not exists yet. But it also represents the status of the async operation.
A Promise can either be resolved or rejected. The .then
method actually accepts two functions as argument. The first
one, for the happy-path, if the operation has resolved. The second one to handle any error that may have occured.
mysteriousAsyncOperation()
.then(
handleSuccess,
handleError
);
Because Promises are often chained together, there is also a .catch
method that accepts a function to handle the first
error that occurs, breaking the chain.
mysteriousAsyncOperation()
.then(secondMysteriousAsyncOperation)
.catch(handleError);
A rejected Promise that has been "caught" always returns a resolved Promise.
mysteriousAsyncOperation()
.then(secondMysteriousAsyncOperation)
.catch(error => alert("¯\_(ツ)_/¯"))
.then(() => alert("Everything is fine actually."));
Now, let's go back to our previous example with multiple inter-dependant async operations...
const $user = retrieveCurrentUser();
const $threads = userPromise.then(
user => setCurrentUserStatus(user.ID, "active")
.then(() => retriveActiveThreadsForUser(user.ID, 10))
);
Promise.all([ $user, $threads ])
.then(([ user, threads ]) => Promise.all(threads.map(thread => subscribeToThread(thread.ID, user.ID))))
.catch(error => alert("Something went wrong."));
And from then on, $user
and $threads
still represent the initial values and can be used again and again without any
unnecessary nesting.
$threads.then(threads => threads.forEach(thread => {
const e = document.createElement("iy-thread");
e.value = thread;
document.body.appendChild(e);
}));
Through my examples, you might have noticed that you can factorize a resolved Promise with the Promise.resolve
function. You can deduct that there's also a Promise.reject
function. These functions are useful when you need a quick
way to get a Promise.
But, if you want to create a Promise from an async operation you will need the Promise constructor.
function wait (d) {
return new Promise(resolve => setTimeout(resolve), d);
}
wait(1000)
.then(() => alert("Waited one second..."));
The Promise constructor handler function also passes a reject
function as the second argument.
function waitOrThrow (d) {
return new Promise((resolve, reject) => {
if (Math.random() > 0.5) reject(new Error("Better change next time."));
else setTimeout(resolve, d);
});
}
waitOrThrow(1000)
.then(
handleSuccess,
handleError
);
A Promise is a data structure that represents any type of value which may or may not exists yet.
The Promise protects the value to be accessed directly.
A handler function can be defined to access and transform the value.
When the handler function returns a value, it creates a new Promise for this value.
In modern JavaScript, understanding and mastering Promises is a very important skill!
They look at lot more scary than they are. But I swear, Promises are your friend.
Posted on May 11, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 26, 2024
November 26, 2024