DEV Community

loading...
Cover image for Promises aren't just a way to deal with async operations...

Promises aren't just a way to deal with async operations...

sebastienfilion profile image Sebastien Filion ・5 min read

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");
Enter fullscreen mode Exit fullscreen mode

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)));
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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
  );
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

A rejected Promise that has been "caught" always returns a resolved Promise.

mysteriousAsyncOperation()
  .then(secondMysteriousAsyncOperation)
  .catch(error => alert("¯\_(ツ)_/¯‍"))
  .then(() => alert("Everything is fine actually."));
Enter fullscreen mode Exit fullscreen mode

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."));
Enter fullscreen mode Exit fullscreen mode

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);
}));
Enter fullscreen mode Exit fullscreen mode

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..."));
Enter fullscreen mode Exit fullscreen mode

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
  );
Enter fullscreen mode Exit fullscreen mode

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.

Discussion (0)

Forem Open with the Forem app