DEV Community

Nik
Nik

Posted on • Updated on

101 series: Promises #1: how promises work

TL&DR picture:
Promise flow

Promise is a then-able object. JS has native Promise impl, which is aligned with Promises/A+ standard https://promisesaplus.com/

new Promise((resolve, reject) => {...})
Enter fullscreen mode Exit fullscreen mode

The constructor receives 2 arguments: resolve to fulfill the promise and reject to reject it.

๐Ÿ“ Promise can be in 1 of 3 different states: pending, fulfilled, rejected.

3 states of promises

๐Ÿ“ Once a promise changes its state from pending to either fulfilled or rejected, it cannot be changed again
Pending => fulfilled, pending => rejected

const result = new Promise((resolve, reject) => {
  resolve(1); // pending => fulfilled with value === 1
  resolve(2); // promise is already fulfilled. No effect
  resolve(3); // promise is already fulfilled. No effect
});

result.then((value) => console.log(value)); // 1
Enter fullscreen mode Exit fullscreen mode

Here we use .then, one of the most important elements in promises:

function onResolve(value) {}
function onReject(reason) {}
new Promise((resolve, reject) => {...}).then(onResolve, onReject)
Enter fullscreen mode Exit fullscreen mode

then callback allows you to execute code when the previous promise is fulfilled or rejected.

๐Ÿ“ Every .then callback can be called only once. There are 2 possible scenarios

  1. Promise gets fulfilled
  2. Promise was fulfilled before we call .then, so the callback will be executed in microtask after we reach the end of the current task and execute previously planned microtasks
// we enter to the main task
let resolve; 
const result = new Promise((_resolve) => {
  resolve = _resolve;
});

setTimeout(() => {console.log(3)}, 0); // Plan a timeout, 
// which will be executed in a separate macro task

result.then(value => console.log(1)); // Plan an onResolve callback 
// to the promise, which is in pending state

resolve('Hey'); // resolve the promise with the value == 'Hey'
// At this point, all previously planned `.then` callbacks 
// will be placed in microtask queue. 
// We will have microtask queue:
// value => console.log(1);

result.then(value => console.log(2)); // Put a new callback
// Microtask queue:
// value => console.log(1);
// value => console.log(2)

// End of macrotask
// Execute 2 microtasks one after another and print 1, 2
// End of all planned microtasks
// Execute a new macrotask from timeout
// print 3
Enter fullscreen mode Exit fullscreen mode

๐Ÿ“ Every single then call returns a new promise instance. It allows us to "chain" promises:
Every single .then returns separate promise

๐Ÿ“ .then callbacks are executed in microtask queue:

If we have 3 macrotasks in the queue they will be executed one after another:
3 macrotasks in execution queue

However if we plan a microtask during the execution one of the tasks this microtask will be executed right after current macrotask:
Second task planned a microtask, which will be executed right after the second task but before the third one
If code plans a microtask during the second task execution, this microtask will be executed right after the second task end, but before the third task, despite the third task might be placed in a queue earlier.

๐Ÿ“ Microtasks postpones macrotask execution

An infinite promise chain may "freeze" the tab:

function freeze(value) {
  console.log(value)
  return Promise.resolve(value + 1)
    .then(freeze); // We have async operation here
  // but it plans a new microtask, 
  // which prevents any other macrotask from the execution,
  // including UI updates
}
freeze(1);
Enter fullscreen mode Exit fullscreen mode

As promises are executed in microtask queue, they have slightly different error handling.
Let's take a closer look at it.
To handle an error we can either provide the second callback to .then method or use .catch.
Generally speaking .catch is an alias for .then without the first argument:

.catch(onReject)
// is equal to:
.then(value => value, onReject)
Enter fullscreen mode Exit fullscreen mode

๐Ÿ“ When promise gets rejected, it ignores all onResolve callbacks up to the first onReject handler

Promise.reject('fail')
 .then(value => console.log(1)) // Nothing happens
 .then(value => console.log(2)) // Nothing happens
 .catch(reason => console.log(reason)) // prints "fail"
Enter fullscreen mode Exit fullscreen mode

๐Ÿ“ .catch, .then with onReject callback returns a promise. If you don't reject promise again, it will be fulfilled:

Promise.reject('fail')
 .catch(reason => console.log(reason)) // prints "fail", returns `undefined`
 .then(value => console.log(value)) // prints `undefined`
 .catch(reason => console.log('fail 2')) // Nothing happens, promise is resolved
Enter fullscreen mode Exit fullscreen mode

It's similar to try{}catch(e) {} blocks. If you get into catch(e) {} block you have to re-throw an error, if you want to handle it later. The same works with promises.

๐Ÿ“ Instead of error event, unhandled promise rejections create unhandledrejection event

globalThis.addEventListener("unhandledrejection", (event) => {
  console.warn(`unhandledrejection: ${event.reason}`);
});
Promise.reject('test');

// Would print: "unhandledrejection: test"
Enter fullscreen mode Exit fullscreen mode

๐Ÿ“ Throw in microtask code would reject the promise:

Promise.resolve(1)
  .then(value => {throw value + 1})
  .catch(reason => console.log(reason)) // prints 2
Enter fullscreen mode Exit fullscreen mode

Alternatively you can return Promise.reject:

Promise.resolve(1)
  .then(value => Promise.reject(value + 1))
  .catch(reason => console.log(reason)) // prints 2
Enter fullscreen mode Exit fullscreen mode

When you resolve a promise, resolving it with Promise.reject would also reject it:

new Promise(resolve => resolve(Promise.reject(1)))
  .then(value => Promise.reject(2))
  .catch(reason => console.log(reason)) // prints 1
Enter fullscreen mode Exit fullscreen mode

Utility methods

๐Ÿ“ Promise.all allows you to await before all the promises change their state to fulfilled, or at least one promise gets rejected

const a = Promise.resolve(1);
const b = new Promise((resolve) => {
  setTimeout(() => resolve('foo'), 1000);
});
const c = Promise.resolve('bar');

Promise.all([a, b, c]).then(console.log); // [1, 'foo', 'bar']
Enter fullscreen mode Exit fullscreen mode

Promise all

If any of the promises gets rejected, Promise.all will be rejected too:

const a = Promise.reject(1); // Now we reject the promise
const b = new Promise((resolve) => {
  setTimeout(() => resolve('foo'), 1000);
});
const c = Promise.resolve('bar');

Promise.all([a, b, c])
  .then(console.log) // Nothing
  .catch(console.log); // 1
Enter fullscreen mode Exit fullscreen mode

๐Ÿ“ Promise.allSettled awaits all promises to change their state. It returns an array with values and the promise statuses

const a = Promise.reject(1); // rejected promise

// 2 resolved promises
const b = new Promise((resolve) => {
  setTimeout(() => resolve('foo'), 1000);
});
const c = Promise.resolve('bar');

Promise.allSettled([a, b, c]).then(console.log); 
// [
//   {status: 'rejected', reason: 1}
//   {status: 'fulfilled', value: 'foo'}
//   {status: 'fulfilled', value: 'bar'}
// ]
Enter fullscreen mode Exit fullscreen mode

๐Ÿ“ Promise.race() method returns a promise that fulfills or rejects as soon as one of the promises fulfills or rejects with the value or reason from that promise

const a = Promise.reject(1); // rejected promise

// 2 resolved promises
const b = new Promise((resolve) => {
  setTimeout(() => resolve('foo'), 1000);
});
const c = Promise.resolve('bar');

Promise.race([a, b, c])
  .then(console.log) // nothing, as the first promise `a` is rejected
  .catch(console.log) // 1
Enter fullscreen mode Exit fullscreen mode

๐Ÿ“ If you put several already fulfilled or rejected promises to the Promise.race, Promise.race will depend on the order of the elements.

So if we change Promise.race([a,b,c]) from the first example, to Promise.race([b,c,a]), the returned promise will be fulfilled with 'bar' value:

const a = Promise.reject(1); // rejected promise

// 2 resolved promises
const b = new Promise((resolve) => {
  setTimeout(() => resolve('foo'), 1000);
});
const c = Promise.resolve('bar');

// We put 'a' at the very end
Promise.race([b, c, a])
  .then(console.log) // 'bar'
  .catch(console.log) // nothing
Enter fullscreen mode Exit fullscreen mode

You can use this trick to test if the promise you have is already resolved or fulfilled.


Summing it up, this article covers base promise mechanisms and execution details. The following articles will cover concurrent and sequential execution, garbage collection, and some of the experiments on top of thenable objects.

Discussion (1)

Collapse
smlka profile image
Andrey Smolko

Man, I just love your content!=)