JavaScript Promises #1: how promises work
Tags: javascript, promise, beginners, programming
Published: 2022-08-01 08:10:00

TL&DR picture:
![Promise flow](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/461bijbd4hsmhawihi7s.png) 

Promise is a then-able object. JS has native Promise impl, which is aligned with Promises/A+ standard https://promisesaplus.com/
```js
new Promise((resolve, reject) => {...})
```
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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tsbiz54xhbj6nlkgywbb.png)
 
📝 Once a promise changes its state from pending to either fulfilled or rejected, it cannot be changed again
![Pending => fulfilled, pending => rejected](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/aqjwbm1vsl2vh1fjd9us.png) 
```js
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
```
Here we use `.then`, one of the most important elements in promises:
```js
function onResolve(value) {}
function onReject(reason) {}
new Promise((resolve, reject) => {...}).then(onResolve, onReject)
```
`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
```js
// 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
```
 
📝 Every single `then` call returns a new promise instance. It allows us to "chain" promises:
![Every single .then returns separate promise](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tg8oqsjw1m925wkf8cjf.png)

📝 `.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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xi63zyxkzn55c7uup5nf.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/64s0bi5rxzuhym1h6yh6.png)
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:
```js
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);
```

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:
```js
.catch(onReject)
// is equal to:
.then(value => value, onReject)
```

📝 When promise gets rejected, it ignores all `onResolve` callbacks up to the first `onReject` handler
```js
Promise.reject('fail')
 .then(value => console.log(1)) // Nothing happens
 .then(value => console.log(2)) // Nothing happens
 .catch(reason => console.log(reason)) // prints "fail"
```

📝 `.catch`, `.then` with onReject callback returns a promise. If you don't reject promise again, it will be fulfilled:

```js
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
```

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

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

// Would print: "unhandledrejection: test"
```

📝 Throw in microtask code would reject the promise:
```js
Promise.resolve(1)
  .then(value => {throw value + 1})
  .catch(reason => console.log(reason)) // prints 2
```

Alternatively you can return Promise.reject:
```js
Promise.resolve(1)
  .then(value => Promise.reject(value + 1))
  .catch(reason => console.log(reason)) // prints 2
```

When you resolve a promise, resolving it with Promise.reject would also reject it:
```js
new Promise(resolve => resolve(Promise.reject(1)))
  .then(value => Promise.reject(2))
  .catch(reason => console.log(reason)) // prints 1
```

### Utility methods
📝 Promise.all allows you to await before all the promises change their state to fulfilled, or at least one promise gets rejected

```js
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']
```

![Promise all](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wtkug033er3sehobkiww.png)

If any of the promises gets rejected, Promise.all will be rejected too:
```js
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
``` 

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

```js
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'}
// ]
```

📝 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

```js
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
```
📝 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:

```js
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
```
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._