Skip to content

Commit 36001d7

Browse files
Improve promise docs (#2672)
1 parent 8b24965 commit 36001d7

File tree

1 file changed

+223
-56
lines changed

1 file changed

+223
-56
lines changed

exercises/concept/translation-service/.docs/introduction.md

Lines changed: 223 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,104 +1,271 @@
11
# Introduction
22

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

6-
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.
5+
<!-- prettier-ignore -->
6+
~~~exercism/note
7+
This is a hard topic for many people, specially if you know programming in a language that is completely _synchronous_.
8+
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".
79
8-
For example:
10+
[talk-slides]: https://go.dev/talks/2012/waza.slide#1
11+
[talk-blog]: https://go.dev/blog/waza-talk
12+
[talk-video]: https://vimeo.com/49718712
13+
~~~
14+
15+
## Lifecycle of a promise
16+
17+
A `Promise` has three states:
18+
19+
1. pending
20+
2. fulfilled
21+
3. rejected
22+
23+
When it is created, a promise is pending.
24+
At some point in the future it may _resolve_ or _reject_.
25+
Once a promise is resolved or rejected once, it can never be resolved or rejected again, nor can its state change.
26+
27+
In other words:
28+
29+
1. When pending, a promise:
30+
- may transition to either the fulfilled or rejected state.
31+
2. When fulfilled, a promise:
32+
- must not transition to any other state.
33+
- must have a value, which must not change.
34+
3. When rejected, a promise:
35+
- must not transition to any other state.
36+
- must have a reason, which must not change.
37+
38+
## Resolving a promise
39+
40+
A promise may be resolved in various ways:
941

1042
```javascript
11-
const myPromise = new Promise(function (resolve, reject) {
12-
let sampleData = [2, 4, 6, 8];
13-
let randomNumber = Math.ceil(Math.random() * 5);
14-
if (sampleData[randomNumber]) {
15-
resolve(sampleData[randomNumber]);
16-
} else {
17-
reject('An error occurred!');
18-
}
43+
// Creates a promise that is immediately resolved
44+
Promise.resolve(value);
45+
46+
// Creates a promise that is immediately resolved
47+
new Promise((resolve) => {
48+
resolve(value);
1949
});
2050

21-
myPromise
22-
.then(function (e) {
23-
console.log(e);
24-
})
25-
.catch(function (error) {
26-
throw new Error(error);
27-
})
28-
.finally(function () {
29-
console.log('Promise completed');
30-
});
51+
// Chaining a promise leads to a resolved promise
52+
somePromise.then(() => {
53+
// ...
54+
return value;
55+
});
56+
```
57+
58+
In the examples above `value` can be _anything_, including an error, `undefined`, `null` or another promise.
59+
Usually you want to resolve with a value that's not an error.
60+
61+
## Rejecting a promise
62+
63+
A promise may be rejected in various ways:
64+
65+
```javascript
66+
// Creates a promise that is immediately rejected
67+
Promise.reject(reason)
68+
69+
// Creates a promise that is immediately rejected
70+
new Promise((_, reject) {
71+
reject(reason)
72+
})
73+
74+
// Chaining a promise with an error leads to a rejected promise
75+
somePromise.then(() => {
76+
// ...
77+
throw reason
78+
})
3179
```
3280

33-
## Methods
81+
In the examples above `reason` can be _anything_, including an error, `undefined` or `null`.
82+
Usually you want to reject with an error.
83+
84+
## Chaining a promise
85+
86+
A promise may be _continued_ with a future action once it resolves or rejects.
3487

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

37-
**then**
92+
### **then**
3893

39-
> 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]
94+
Every promise is "thenable".
95+
That means that there is a function `then` available that will be executed once the original promise is resolves.
96+
Given `promise.then(onResolved)`, the callback `onResolved` receives the value the original promise was resolved with.
97+
This will always return a _new_ "chained" promise.
98+
99+
Returning a `value` from `then` resolves the "chained" promise.
100+
Throwing a `reason` in `then` rejects the "chained" promise.
40101

41102
```javascript
42103
const promise1 = new Promise(function (resolve, reject) {
43-
resolve('Success!');
104+
setTimeout(() => {
105+
resolve('Success!');
106+
}, 1000);
44107
});
45108

46-
promise1.then(function (value) {
109+
const promise2 = promise1.then(function (value) {
47110
console.log(value);
48111
// expected output: "Success!"
112+
113+
return true;
49114
});
50115
```
51116

52-
**catch**
117+
This will log `"Success!"` after approximately 1000 ms.
118+
The state & value of `promise1` will be `resolved` and `"Success!"`.
119+
The state & value of `promise2` will be `resolved` and `true`.
53120

54-
> 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]
121+
There is a second argument available that runs when the original promise rejects.
122+
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.
55123

56124
```javascript
57-
const promise1 = new Promise((resolve, reject) => {
58-
throw 'An error occurred';
59-
});
125+
const promise1 = new Promise(function (resolve, reject) {
126+
setTimeout(() => {
127+
resolve('Success!');
128+
}, 1000);
60129

61-
promise1.catch(function (error) {
62-
console.error(error);
130+
if (Math.random() < 0.5) {
131+
reject('Nope!');
132+
}
63133
});
64-
// expected output: An error occurred
134+
135+
function log(value) {
136+
console.log(value);
137+
return true;
138+
}
139+
140+
function shout(reason) {
141+
console.error(reason.toUpperCase());
142+
return false;
143+
}
144+
145+
const promise2 = promise1.then(log, shout);
65146
```
66147

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

69-
> 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]
155+
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.
156+
It is important to understand that returning a value from a promise resolves it, and throwing a value rejects it.
157+
When `promise1` resolves and there is a chained `onResolved`: `then(onResolved)`, then that follow-up is a new promise that can resolve or reject.
158+
When `promise1` rejects but there is a chained `onRejected`: `then(, onRejected)`, then that follow-up is a new promise that can resolve or reject.
159+
160+
### **catch**
161+
162+
Sometimes you want to capture errors and only continue when the original promise `reject`s.
163+
Given `promise.catch(onCatch)`, the callback `onCatch` receives the reason the original promise was rejected.
164+
This will always return a _new_ "chained" promise.
165+
166+
Returning a `value` from `catch` resolves the "chained" promise.
167+
Throwing a `reason` in `catch` rejects the "chained" promise.
70168

71169
```javascript
72-
function findDataById(id) {
73-
return new Promise(function (resolve, reject) {
74-
let sampleData = [1, 2, 3, 4, 5];
75-
if (sampleData[id]) {
76-
resolve(sampleData[id]);
77-
} else {
78-
reject(new Error('Invalid id'));
79-
}
80-
});
170+
const promise1 = new Promise(function (resolve, reject) {
171+
setTimeout(() => {
172+
resolve('Success!');
173+
}, 1000);
174+
175+
if (Math.random() < 0.5) {
176+
reject('Nope!');
177+
}
178+
});
179+
180+
function log(value) {
181+
console.log(value);
182+
return 'done';
183+
}
184+
185+
function recover(reason) {
186+
console.error(reason.toUpperCase());
187+
return 42;
81188
}
82189

83-
findDataById(4)
84-
.then(function (response) {
85-
console.log(response);
190+
const promise2 = promise1.catch(recover).then(log);
191+
```
192+
193+
In about 1/2 of the cases, this will log `"Success!"` after approximately 1000 ms.
194+
In the other 1/2 of the cases, this will immediately log `42`.
195+
196+
- If `promise1` resolves, `catch` is skipped and it reaches `then`, and logs the value.
197+
- The state & value of `promise1` will be `resolved` and `"Success!"`.
198+
- The state & value of `promise2` will be `resolved` and `"done"`;
199+
- 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.
200+
- The state & value of `promise1` will be `rejected` and `"Nope!"`.
201+
- The state & value of `promise2` will be `resolved` and `"done"`;
202+
203+
### **finally**
204+
205+
Sometimes you want to execute code after a promise settles, regardless if the promise resolves or rejects.
206+
Given `promise.finally(onSettled)`, the callback `onSettled` receives nothing.
207+
This will always return a _new_ "chained" promise.
208+
209+
Returning a `value` from `finally` copies the status & value from the original promise, ignoring the `value`.
210+
Throwing a `reason` in `finally` rejects the "chained" promise, overwriting any status & value or reason from the original promise.
211+
212+
## Example
213+
214+
Various of the methods together:
215+
216+
```javascript
217+
const myPromise = new Promise(function (resolve, reject) {
218+
const sampleData = [2, 4, 6, 8];
219+
const randomNumber = Math.round(Math.random() * 5);
220+
221+
if (sampleData[randomNumber]) {
222+
resolve(sampleData[randomNumber]);
223+
} else {
224+
reject('Sampling did not result in a sample');
225+
}
226+
});
227+
228+
const finalPromise = myPromise
229+
.then(function (sampled) {
230+
// If the random number was 0, 1, 2, or 3, this will be
231+
// reached and the number 2, 4, 6, or 8 will be logged.
232+
console.log(`Sampled data: ${sampled}`);
233+
return 'yay';
86234
})
87-
.catch(function (err) {
88-
console.error(err);
235+
.catch(function (reason) {
236+
// If the random number was 4 or 5, this will be reached and
237+
// reason will be "An error occurred". The entire chain will
238+
// then reject with an Error with the reason as message.
239+
throw new Error(reason);
89240
})
90241
.finally(function () {
242+
// This will always log after either the sampled data is
243+
// logged or the error is raised.
91244
console.log('Promise completed');
92245
});
93246
```
94247

95-
---
248+
- In the cases `randomNumber` is `0-3`:
249+
- `myPromise` will be resolved with the value `2, 4, 6, or 8`
250+
- `finalPromise` will be resolved with the value `'yay'`
251+
- There will be two logs:
252+
- `Sampled data: ...`
253+
- `Promise completed`
254+
- In the cases `randomNumber` is `4-5`:
255+
- `myPromise` will be rejected with the reason `'Sampling did not result in a sample'`
256+
- `finalPromise` will be rejected with the reason `Error('Sampling did not result in a sample')`
257+
- There will be one log:
258+
- `Promise completed`
259+
- _in some environments_ this will yield an `"uncaught rejected promise: Error('Sampling did not result in a sample')"` log
96260

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

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

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

0 commit comments

Comments
 (0)