Alxblsk.com Logo
RE•DONE
Blog by Aliaksei Belski

Asynchronous operations in Javascript: Promises

Published: June 3rd, 2020, Updated: September 12th, 2020javascriptPromisesECMAScript2015

Asynchronous operations in Javascript: Promises

From the beginning, Javascript gained additional capabilities of being usefully asynchronous. Thanks to callbacks, all we need is to subscribe to action and pass the so-called call-after function. It worked for years, but this design lacked beauty for relatively complex applications.

Another attempt has been done with ECMAScript 2015, where developers finally got a build-in opportunity to flatten subsequence of actions, designed after a successful asynchronous action (in contrast to classic callbacks, where a nesting level grew tremendously and made applications bulky).

This post is an attempt to provide that bare minimum to work with Promises efficiently (taking into account that such new projects like Deno tightly coupled with them). We’ll also skip most of the history and focus on practice.


To make explanation short, let’s visualize states of a Promise and process resolving it overall.

Imagine that you need to get a value you can’t calculate here and now, for example, a user’s bank account balance. It’s always stored remotely, so we need to make a request to your bank API and ask for the value. While the request is processed, you can still perform all other calculations and paintings, but instead of real numbers, all you can do is to put a nice placeholder with a spinner nearby.

Application, default state

A browser finished with card rendering way faster than a user received a value (we’re checking their balance, no rush) and stopped the execution of respective code. All we need is to return there later, once a value is available. In Javascript, it means we need to prepare a new scope and wait (same as in a bank, an accountant leaves you for a minute while you’re waiting for them on a couch). This is [pending] state.

There are, naturally, two available results: you get the balance (hopefully, positive) [fulfill], or you don’t by some reason (bank server became unavailable, a spouse is watching Netflix in 4K and Internet connection is weak, etc.) [reject]. In this case, a Promise is a convenient tool to finish rendering with an appropriate result (pleasant digits or an error message).

Application, possible states

Think of a Promise as an assistant whom you delegate a task, where all you need is to be involved in handling results, ignoring all intermediate details of implementation.

Let’s process results now

Once your virtual assistant got a result, you need to prepare a queue of subsequent handlers. In our case, several additional things need to be done:

  1. Format value as a currency (₽);
  2. Calculate other currencies based on the amount of money in a wallet ($, €);
  3. Render results.

However, having a list of tasks is not enough to get things done. Though having an error handling is not mandatory, you need a plan anyway. Without it, you put your application at risk of being broken. There are two tactics of handling wrong response behavior:

  • Put a .catch rule first and normalize behavior. When error handled first, right after that Promise starts to behave as if everything is fine, and goes through all the tasks then;
  • Place the .catch rule last, after all the tasks. This scenario will skip all the tasks to the nearest error handler and then normalize Promise state.

Once error appears, it looks for an error handler in a chain and skips all the tasks. After that, Promise's state stabilizes from reject to fulfill and continues to evaluate tasks (if there are any after an error handler).

Promise, visual interpretation

Enough theory

This is an essential promise example:

const balance = new Promise((resolve, reject) => {
  // Defining an asynchronous operation
    setTimeout(() => {
      // generating a number based on a current time 
        const now = Date.now();
      // if value is even, then we simulate successful result
        if (now % 2 === 0) resolve({ balance: '100', currency: '₽' })
      // otherwise - return an error
      else reject({ reason: 'Connection Error' })
    }, 500);
})

Feel free to replace setTimeout with any other asynchronous operation; the principle stays the same:

  • If you detect that operation was completed successfully - call resolve callback and pass a resulted value inside;
  • If an operation fails (or takes too long, or any other failing case) - call reject and pass a reason inside.

Regardless of result, Promise alone behaves like a capsule, and always keeps a value inside. All the chained tasks accept the value but never release it.

Promise chain

Let’s create one task which displays an actual balance, and one error handler which notifies about connection error:

balance
  .then((result) => {
    const { currency, balance } = result;
    console.log(`Your balance is ${currency}${balance}`);
  })
  .catch((fail) => {
    console.warn(fail.reason);
  })

As the case above describes, .then is responsible for success, and .catch is a fallback for an error. They are independent and never be called together. But it’s also possible to design this code differently. Let’s flip the order and see what happens.

balance
  .catch((fail) => {
    console.warn(fail.reason);
      return fail;
  })
  .then((result) => {
    if (result.reason) console.log('Balance: ₽----.--');
    else console.log(`Balance ${currency}${balance}`);
  })

Now, as .catch defined in a first place, it’ll warn about an error, and then passes execution to the task .then, which will handle both cases. In case of a successful request, error handling will be skipped, and control will be delegated right to a first task.

Now it's your turn to try.

More facts about a Promise

  • Promise can return another Promise, waiting for another asynchronous result before moving along.
  • Make sure that every Promise has at least one .catch fallback, to keep your code safe.
  • Because Promise behaves like ‘a capsule’ and encapsulates a value, an error can’t be caught with try/catch operator.
  • Multiple Promise instances can be handled together, thanks to all, allSettled, and race static methods. for more details, please follow the reference.

Other posts in a series