Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
279 changes: 223 additions & 56 deletions exercises/concept/translation-service/.docs/introduction.md
Original file line number Diff line number Diff line change
@@ -1,104 +1,271 @@
# Introduction

The [`Promise`][promise-docs] object represents the eventual completion (or failure) of an
asynchronous operation and its resulting value.
The [`Promise`][promise-docs] object represents the eventual completion (or failure) of an asynchronous operation and its resulting value.

The methods [`promise.then()`][promise-then], [`promise.catch()`][promise-catch], and [`promise.finally()`][promise-finally] are used to associate further action with a promise that becomes settled.
<!-- prettier-ignore -->
~~~exercism/note
This is a hard topic for many people, specially if you know programming in a language that is completely _synchronous_.
If you feel overwhelmed, or you would like to learn more about **concurrency** and **parallelism**, [watch (via go.dev)][talk-blog] or [watch directly via vimeo][talk-video] and [read the slides][talk-slides] of the brilliant talk "Concurrency is not parallelism".
For example:
[talk-slides]: https://go.dev/talks/2012/waza.slide#1
[talk-blog]: https://go.dev/blog/waza-talk
[talk-video]: https://vimeo.com/49718712
~~~

## Lifecycle of a promise

A `Promise` has three states:

1. pending
2. fulfilled
3. rejected

When it is created, a promise is pending.
At some point in the future it may _resolve_ or _reject_.
Once a promise is resolved or rejected once, it can never be resolved or rejected again, nor can its state change.

In other words:

1. When pending, a promise:
- may transition to either the fulfilled or rejected state.
2. When fulfilled, a promise:
- must not transition to any other state.
- must have a value, which must not change.
3. When rejected, a promise:
- must not transition to any other state.
- must have a reason, which must not change.

## Resolving a promise

A promise may be resolved in various ways:

```javascript
const myPromise = new Promise(function (resolve, reject) {
let sampleData = [2, 4, 6, 8];
let randomNumber = Math.ceil(Math.random() * 5);
if (sampleData[randomNumber]) {
resolve(sampleData[randomNumber]);
} else {
reject('An error occurred!');
}
// Creates a promise that is immediately resolved
Promise.resolve(value);

// Creates a promise that is immediately resolved
new Promise((resolve) => {
resolve(value);
});

myPromise
.then(function (e) {
console.log(e);
})
.catch(function (error) {
throw new Error(error);
})
.finally(function () {
console.log('Promise completed');
});
// Chaining a promise leads to a resolved promise
somePromise.then(() => {
// ...
return value;
});
```

In the examples above `value` can be _anything_, including an error, `undefined`, `null` or another promise.
Usually you want to resolve with a value that's not an error.

## Rejecting a promise

A promise may be rejected in various ways:

```javascript
// Creates a promise that is immediately rejected
Promise.reject(reason)

// Creates a promise that is immediately rejected
new Promise((_, reject) {
reject(reason)
})

// Chaining a promise with an error leads to a rejected promise
somePromise.then(() => {
// ...
throw reason
})
```

## Methods
In the examples above `reason` can be _anything_, including an error, `undefined` or `null`.
Usually you want to reject with an error.

## Chaining a promise

A promise may be _continued_ with a future action once it resolves or rejects.

These methods are available on `Promise.prototype`
- [`promise.then()`][promise-then] is called once `promise` resolves
- [`promise.catch()`][promise-catch] is called once `promise` rejects
- [`promise.finally()`][promise-finally] is called once `promise` either resolves or rejects

**then**
### **then**

> The `.then()` method takes up to two arguments; the first argument is a callback function for the resolved case of the promise, and the second argument is a callback function for the rejected case. Each `.then()` returns a newly generated promise object, which can optionally be used for chaining.[^1]
Every promise is "thenable".
That means that there is a function `then` available that will be executed once the original promise is resolves.
Given `promise.then(onResolved)`, the callback `onResolved` receives the value the original promise was resolved with.
This will always return a _new_ "chained" promise.

Returning a `value` from `then` resolves the "chained" promise.
Throwing a `reason` in `then` rejects the "chained" promise.

```javascript
const promise1 = new Promise(function (resolve, reject) {
resolve('Success!');
setTimeout(() => {
resolve('Success!');
}, 1000);
});

promise1.then(function (value) {
const promise2 = promise1.then(function (value) {
console.log(value);
// expected output: "Success!"

return true;
});
```

**catch**
This will log `"Success!"` after approximately 1000 ms.
The state & value of `promise1` will be `resolved` and `"Success!"`.
The state & value of `promise2` will be `resolved` and `true`.

> A `.catch()` is just a `.then()` without a slot for a callback function for the case when the promise is resolved. It is used to handle rejected promises.[^2]
There is a second argument available that runs when the original promise rejects.
Given `promise.then(onResolved, onRejected)`, the callback `onResolved` receives the value the original promise was resolved with, or the callback `onRejected` receives the reason the promise was rejected.

```javascript
const promise1 = new Promise((resolve, reject) => {
throw 'An error occurred';
});
const promise1 = new Promise(function (resolve, reject) {
setTimeout(() => {
resolve('Success!');
}, 1000);

promise1.catch(function (error) {
console.error(error);
if (Math.random() < 0.5) {
reject('Nope!');
}
});
// expected output: An error occurred

function log(value) {
console.log(value);
return true;
}

function shout(reason) {
console.error(reason.toUpperCase());
return false;
}

const promise2 = promise1.then(log, shout);
```

**finally**
- In about 1/2 of the cases, this will log `"Success!"` after approximately 1000 ms.
- The state & value of `promise1` will be `resolved` and `"Success!"`.
- The state & value of `promise2` will be `resolved` and `true`.
- In about 1/2 of the cases, this will immediately log `"NOPE!"`.
- The state & value of `promise1` will be `rejected` and `Nope!`.
- The state & value of `promise2` will be `resolved` and `false`.

> When the promise is settled, i.e either fulfilled or rejected, the specified callback function is executed. This provides a way for code to be run whether the promise was fulfilled successfully or rejected once the Promise has been dealt with.[^3]
It is important to understand that because of the rules of the lifecycle, when it `reject`s, the `resolve` that comes in ~1000ms later is silently ignored, as the internal state cannot change once it has rejected or resolved.
It is important to understand that returning a value from a promise resolves it, and throwing a value rejects it.
When `promise1` resolves and there is a chained `onResolved`: `then(onResolved)`, then that follow-up is a new promise that can resolve or reject.
When `promise1` rejects but there is a chained `onRejected`: `then(, onRejected)`, then that follow-up is a new promise that can resolve or reject.

### **catch**

Sometimes you want to capture errors and only continue when the original promise `reject`s.
Given `promise.catch(onCatch)`, the callback `onCatch` receives the reason the original promise was rejected.
This will always return a _new_ "chained" promise.

Returning a `value` from `catch` resolves the "chained" promise.
Throwing a `reason` in `catch` rejects the "chained" promise.

```javascript
function findDataById(id) {
return new Promise(function (resolve, reject) {
let sampleData = [1, 2, 3, 4, 5];
if (sampleData[id]) {
resolve(sampleData[id]);
} else {
reject(new Error('Invalid id'));
}
});
const promise1 = new Promise(function (resolve, reject) {
setTimeout(() => {
resolve('Success!');
}, 1000);

if (Math.random() < 0.5) {
reject('Nope!');
}
});

function log(value) {
console.log(value);
return 'done';
}

function recover(reason) {
console.error(reason.toUpperCase());
return 42;
}

findDataById(4)
.then(function (response) {
console.log(response);
const promise2 = promise1.catch(recover).then(log);
```

In about 1/2 of the cases, this will log `"Success!"` after approximately 1000 ms.
In the other 1/2 of the cases, this will immediately log `42`.

- If `promise1` resolves, `catch` is skipped and it reaches `then`, and logs the value.
- The state & value of `promise1` will be `resolved` and `"Success!"`.
- The state & value of `promise2` will be `resolved` and `"done"`;
- If `promise1` rejects, `catch` is executed, which _returns a value_, and thus the chain is now `resolved`, and it reaches `then`, and logs the value.
- The state & value of `promise1` will be `rejected` and `"Nope!"`.
- The state & value of `promise2` will be `resolved` and `"done"`;

### **finally**

Sometimes you want to execute code after a promise settles, regardless if the promise resolves or rejects.
Given `promise.finally(onSettled)`, the callback `onSettled` receives nothing.
This will always return a _new_ "chained" promise.

Returning a `value` from `finally` copies the status & value from the original promise, ignoring the `value`.
Throwing a `reason` in `finally` rejects the "chained" promise, overwriting any status & value or reason from the original promise.

## Example

Various of the methods together:

```javascript
const myPromise = new Promise(function (resolve, reject) {
const sampleData = [2, 4, 6, 8];
const randomNumber = Math.round(Math.random() * 5);

if (sampleData[randomNumber]) {
resolve(sampleData[randomNumber]);
} else {
reject('Sampling did not result in a sample');
}
});

const finalPromise = myPromise
.then(function (sampled) {
// If the random number was 0, 1, 2, or 3, this will be
// reached and the number 2, 4, 6, or 8 will be logged.
console.log(`Sampled data: ${sampled}`);
return 'yay';
})
.catch(function (err) {
console.error(err);
.catch(function (reason) {
// If the random number was 4 or 5, this will be reached and
// reason will be "An error occurred". The entire chain will
// then reject with an Error with the reason as message.
throw new Error(reason);
})
.finally(function () {
// This will always log after either the sampled data is
// logged or the error is raised.
console.log('Promise completed');
});
```

---
- In the cases `randomNumber` is `0-3`:
- `myPromise` will be resolved with the value `2, 4, 6, or 8`
- `finalPromise` will be resolved with the value `'yay'`
- There will be two logs:
- `Sampled data: ...`
- `Promise completed`
- In the cases `randomNumber` is `4-5`:
- `myPromise` will be rejected with the reason `'Sampling did not result in a sample'`
- `finalPromise` will be rejected with the reason `Error('Sampling did not result in a sample')`
- There will be one log:
- `Promise completed`
- _in some environments_ this will yield an `"uncaught rejected promise: Error('Sampling did not result in a sample')"` log

[^1]: `then`, MDN. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then
As shown above, `reject` works with a string, and a promise can also reject with an `Error`.

[^2]: `catch`, MDN. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/catch
<!-- prettier-ignore -->
~~~exercism/note
If chaining promises or general usage is unclear, the [tutorial on MDN][mdn-promises] is a good resource to consume.
[^3]: `finally`, MDN. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/finally
[mdn-promises]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises
~~~

[promise-docs]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
[promise-catch]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/catch
Expand Down