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.

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).

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:
- Format value as a currency (₽);
- Calculate other currencies based on the amount of money in a wallet ($, €);
- 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
.catchrule first and normalize behavior. When error handled first, right after thatPromisestarts to behave as if everything is fine, and goes through all the tasks then; - Place the
.catchrule last, after all the tasks. This scenario will skip all the tasks to the nearest error handler and then normalizePromisestate.
Once error appears, it looks for an error handler in a chain and skips all the tasks. After that, Promise's state stabilizes from
rejecttofulfilland continues to evaluate tasks (if there are any after an error handler).

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
resolvecallback and pass a resulted value inside; - If an operation fails (or takes too long, or any other failing case) - call
rejectand pass a reason inside.
Regardless of result,
Promisealone behaves like a capsule, and always keeps a value inside. All the chained tasks accept the value but never release it.

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
Promisecan return anotherPromise, waiting for another asynchronous result before moving along.- Make sure that every
Promisehas at least one.catchfallback, to keep your code safe. - Because
Promisebehaves like ‘a capsule’ and encapsulates a value, an error can’t be caught withtry/catchoperator. - Multiple
Promiseinstances can be handled together, thanks toall,allSettled, andracestatic methods. for more details, please follow the reference.