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.
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:
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:
.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;.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
tofulfill
and continues to evaluate tasks (if there are any after an error handler).
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:
resolve
callback and pass a resulted value inside;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.
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.
Promise
can return another Promise
, waiting for another asynchronous result before moving along.Promise
has at least one .catch
fallback, to keep your code safe.Promise
behaves like ‘a capsule’ and encapsulates a value, an error can’t be caught with try/catch
operator.Promise
instances can be handled together, thanks to all
, allSettled
, and race
static methods. for more details, please follow the reference.