|
1 | 1 | # Introduction |
2 | 2 |
|
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. |
5 | 4 |
|
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". |
7 | 9 |
|
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: |
9 | 41 |
|
10 | 42 | ```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); |
19 | 49 | }); |
20 | 50 |
|
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 | +}) |
31 | 79 | ``` |
32 | 80 |
|
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. |
34 | 87 |
|
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 |
36 | 91 |
|
37 | | -**then** |
| 92 | +### **then** |
38 | 93 |
|
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. |
40 | 101 |
|
41 | 102 | ```javascript |
42 | 103 | const promise1 = new Promise(function (resolve, reject) { |
43 | | - resolve('Success!'); |
| 104 | + setTimeout(() => { |
| 105 | + resolve('Success!'); |
| 106 | + }, 1000); |
44 | 107 | }); |
45 | 108 |
|
46 | | -promise1.then(function (value) { |
| 109 | +const promise2 = promise1.then(function (value) { |
47 | 110 | console.log(value); |
48 | 111 | // expected output: "Success!" |
| 112 | + |
| 113 | + return true; |
49 | 114 | }); |
50 | 115 | ``` |
51 | 116 |
|
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`. |
53 | 120 |
|
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. |
55 | 123 |
|
56 | 124 | ```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); |
60 | 129 |
|
61 | | -promise1.catch(function (error) { |
62 | | - console.error(error); |
| 130 | + if (Math.random() < 0.5) { |
| 131 | + reject('Nope!'); |
| 132 | + } |
63 | 133 | }); |
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); |
65 | 146 | ``` |
66 | 147 |
|
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`. |
68 | 154 |
|
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. |
70 | 168 |
|
71 | 169 | ```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; |
81 | 188 | } |
82 | 189 |
|
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'; |
86 | 234 | }) |
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); |
89 | 240 | }) |
90 | 241 | .finally(function () { |
| 242 | + // This will always log after either the sampled data is |
| 243 | + // logged or the error is raised. |
91 | 244 | console.log('Promise completed'); |
92 | 245 | }); |
93 | 246 | ``` |
94 | 247 |
|
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 |
96 | 260 |
|
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`. |
98 | 262 |
|
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. |
100 | 266 |
|
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 | +~~~ |
102 | 269 |
|
103 | 270 | [promise-docs]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise |
104 | 271 | [promise-catch]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/catch |
|
0 commit comments