You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: 8-async/02-promise-basics/article.md
+18-3Lines changed: 18 additions & 3 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -113,8 +113,8 @@ let promise = new Promise(function(resolve, reject) {
113
113
For instance, it happens when we start to do a job and then see that everything has already been done. Technically that's fine: we have a resolved promise right now.
114
114
````
115
115
116
-
````smart header="Promise properties are internal"
117
-
Properties of promise objects like `state` and `result` are internal. We can't directly access them from our code.
116
+
```smart header="The `state` and `result` are internal"
117
+
Properties `state` and `result` of a promise object are internal. We can't directly access them from our code, but we can use methods `.then/catch` for that, they are described below.
118
118
```
119
119
120
120
## Consumers: ".then" and ".catch"
@@ -200,10 +200,25 @@ If a promise is pending, `.then/catch` handlers wait for the result. Otherwise,
200
200
// an immediately resolved promise
201
201
let promise = new Promise(resolve => resolve("done!"));
202
202
203
-
promise.then(alert); // done! (without a delay)
203
+
promise.then(alert); // done! (shows up right now)
204
204
```
205
205
206
206
That's good for jobs that may sometimes require time and sometimes finish immediately. The handler is guaranteed to run in both cases.
207
+
208
+
To be precise, `.then/catch` queue up and are taken from the queue asynchronously when the current code finishes, like `setTimeout(..., 0)`.
209
+
210
+
So here the `alert` call is "queued" and runs immediately after the code finishes:
211
+
212
+
```js run
213
+
// an immediately resolved promise
214
+
let promise = new Promise(resolve => resolve("done!"));
215
+
216
+
promise.then(alert); // done! (right after the current code finishes)
217
+
218
+
alert("code finished"); // this alert shows first
219
+
```
220
+
221
+
In practice the time for the code to finish execution is usually negligible. But the code after `.then` always executes before the `.then` handler (even in the case of a pre-resolved promise), that could matter.
207
222
````
208
223
209
224
Now let's see more practical examples to see how promises can help us in writing asynchronous code.
@@ -189,27 +189,63 @@ This code does the same: loads 3 scripts in sequence. But it "grows to the right
189
189
190
190
Sometimes it's ok to write `.then` directly, because the nested function has access to the outer scope `(*)`, but that's an exception rather than a rule.
191
191
192
+
193
+
````smart header="Thenables"
194
+
To be precise, `.then` may return an arbitrary "thenable" object, and it will be treated the same way as a promise.
195
+
196
+
A "thenable" object is any object with a method `.then`.
197
+
198
+
The idea is that 3rd-party libraries may implement "promise-compatible" objects of their own. They can have extended set of methods, but also be compatible with native promises, because they implement `.then`.
199
+
200
+
Here's an example of a thenable object:
201
+
202
+
```js run
203
+
class Thenable {
204
+
constructor(result, delay) {
205
+
this.result = result;
206
+
}
207
+
// then method has the similar signature to promises
208
+
then(resolve, reject) {
209
+
// resolve with double this.result after the delay
JavaScript checks the object returned by the handler in the line `(*)`: it it has a callable method named `then`, then it waits until that method is called, and the result is passed further.
222
+
223
+
That allows to integrate side objects with promise chains without having to inherit from `Promise`.
224
+
````
225
+
226
+
192
227
## Bigger example: fetch
193
228
194
-
In frontend programming promises are often used for network requests. So let's make an example of that.
229
+
In frontend programming promises are often used for network requests. So let's see an extended example of that.
195
230
196
231
We'll use the [fetch](mdn:api/WindowOrWorkerGlobalScope/fetch) method to load the information about the user from the remote server. The method is quite complex, it has many optional parameters, but the basic usage is quite simple:
197
232
198
233
```js
199
234
let promise =fetch(url);
200
235
```
201
236
202
-
Makes a network request to the `url` and returns a promise. The promise resolves with a `response` object when the remote server responds with headers, but before the full response is downloaded.
237
+
This makes a network request to the `url` and returns a promise. The promise resolves with a `response` object when the remote server responds with headers, but before the full response is downloaded.
203
238
204
-
To read the full response, we should call a method `response.text()`: it returns a promise that resolves with the full response text when it's downloaded from the remote server.
239
+
To read the full response, we should call a method `response.text()`: it returns a promise that resolves when the full text downloaded from the remote server, and has that text as a result.
205
240
206
-
The code below makes a request to `user.json` and then loads it as text from the server:
241
+
The code below makes a request to `user.json` and loads its text from the server:
207
242
208
243
```js run
209
244
fetch('/article/promise-chaining/user.json')
210
-
// .then runs when the remote server responds
245
+
// .then below runs when the remote server responds
211
246
.then(function(response) {
212
-
// response.text() is the new promise that resolves when the server finishes sending data
247
+
// response.text() returns a new promise that resolves with the full response text
248
+
// when we finish downloading it
213
249
returnresponse.text();
214
250
})
215
251
.then(function(text) {
@@ -223,12 +259,15 @@ There is also a method `response.json()` that reads the remote data and parses i
223
259
We'll also use arrow functions for brevity:
224
260
225
261
```js run
262
+
// same as above, but response.json() parses the remote content as JSON
226
263
fetch('/article/promise-chaining/user.json')
227
264
.then(response=>response.json())
228
265
.then(user=>alert(user.name)); // iliakan
229
266
```
230
267
231
-
Now let's do something with it. For instance, we can make one more request to github, load the user profile and show the avatar:
268
+
Now let's do something with the loaded user.
269
+
270
+
For instance, we can make one more request to github, load the user profile and show the avatar:
The code works. But there's a potential problem in it.
292
+
The code works. But there's a potential problem in it, a typical error of those who begin to use promises.
254
293
255
-
Look at the line `(*)`: how can we do something *after* the avatar is removed? For instance, we'd like to show a form for editing that user or something else.
294
+
Look at the line `(*)`: how can we do something *after* the avatar is removed? For instance, we'd like to show a form for editing that user or something else. As of now, there's no way.
256
295
257
296
To make the chain extendable, we need to return a promise that resolves when the avatar finishes showing.
Now when `setTimeout` runs the function, it calls `resolve(githubUser)`, thus passing the control to the next `.then` in the chain and passing forward the user data.
284
325
285
-
As a rule, an asynchronous action should always return a promise. That makes possible to plan actions after it. Even if we don't plan to extend the chain now, we may need it later.
326
+
As a rule, an asynchronous action should always return a promise.
327
+
328
+
That makes possible to plan actions after it. Even if we don't plan to extend the chain now, we may need it later.
286
329
287
330
Finally, we can split the code into reusable functions:
288
331
@@ -311,6 +354,7 @@ function showAvatar(githubUser) {
311
354
});
312
355
}
313
356
357
+
// Use them:
314
358
loadJson('/article/promise-chaining/user.json')
315
359
.then(user=>loadGithubUser(user.name))
316
360
.then(showAvatar)
@@ -476,15 +520,102 @@ new Promise(function(resolve, reject) {
476
520
477
521
The handler `(*)` catches the error and just can't handle it, because it's not `URIError`, so it throws it again. Then the execution jumps to the next `.catch` down the chain `(**)`.
478
522
479
-
## Unhandled rejections
523
+
In the section below we'll see a practical example of rethrowing.
524
+
525
+
## Fetch error handling example
526
+
527
+
Let's improve error handling for the user-loading example.
528
+
529
+
The promise returned by [fetch](mdn:api/WindowOrWorkerGlobalScope/fetch) rejects when it's impossible to make a request. For instance, a remote server is not available, or the URL is malformed. But if the remote server responds with error 404, or even error 500, then it's considered a valid response.
530
+
531
+
What if the server returns a non-JSON page with error 500 in the line `(*)`? What if there's no such user, and github returns a page with error 404 at `(**)`?
.catch(alert); // SyntaxError: Unexpected token < in JSON at position 0
539
+
// ...
540
+
```
541
+
542
+
543
+
As of now, the code tries to load the response as JSON no matter what and dies with a syntax error. You can see that by running the example above, as the file `no-such-user.json` doesn't exist.
544
+
545
+
That's not good, because the error just falls through the chain, without details: what failed and where.
546
+
547
+
So let's add one more step: we should check the `response.status` property that has HTTP status, and if it's not 200, then throw an error.
480
548
481
-
...But what if we forget to append an error handler to the end of the chain?
549
+
```js run
550
+
classHttpErrorextendsError { // (1)
551
+
constructor(response) {
552
+
super(`${response.status} for ${response.url}`);
553
+
this.name='HttpError';
554
+
this.response= response;
555
+
}
556
+
}
557
+
558
+
functionloadJson(url) { // (2)
559
+
returnfetch(url)
560
+
.then(response=> {
561
+
if (response.status==200) {
562
+
returnresponse.json();
563
+
} else {
564
+
thrownewHttpError(response);
565
+
}
566
+
})
567
+
}
568
+
569
+
loadJson('no-such-user.json') // (3)
570
+
.catch(alert); // HttpError: 404 for .../no-such-user.json
571
+
```
572
+
573
+
1. We make a custom class for HTTP Errors to distinguish them from other types of errors. Besides, the new class has a constructor that accepts the `response` object and saves it in the error. So error-handling code will be able to access it.
574
+
2. Then we put together the requesting and error-handling code into a function that fetches the `url`*and* treats any non-200 status as an error. That's convenient, because we often need such logic.
575
+
3. Now `alert` shows better message.
482
576
483
-
Like here:
577
+
The great thing about having our own class for errors is that we can easily check for it in error-handling code.
578
+
579
+
For instance, we can make a request, and then if we get 404 -- ask the user to modify the information.
580
+
581
+
The code below loads a user with the given name from github. If there's no such user, then it asks for the correct name:
if (err instanceof HttpError &&err.response.status==404) { // (2)
595
+
*/!*
596
+
alert("No such user, please reenter.");
597
+
returndemoGithubUser();
598
+
} else {
599
+
throw err;
600
+
}
601
+
});
602
+
}
603
+
604
+
demoGithubUser();
605
+
```
606
+
607
+
Here:
608
+
609
+
1. If `loadJson` returns a valid user object, then the name is shown `(1)`, and the user is returned, so that we can add more user-related actions to the chain. In that case the `.catch` below is ignored, everything's very simple and fine.
610
+
2. Otherwise, in case of an error, we check it in the line `(2)`. Only if it's indeed the HTTP error, and the status is 404 (Not found), we ask the user to reenter. For other errors -- we don't know how to handle, so we just rethrow them.
611
+
612
+
## Unhandled rejections
613
+
614
+
What happens when an error is not handled? For instance, after the rethrow as in the example above. Or if we forget to append an error handler to the end of the chain, like here:
484
615
485
616
```js untrusted run refresh
486
617
newPromise(function() {
487
-
errorHappened(); // Error here (no such function)
618
+
noSuchFunction(); // Error here (no such function)
488
619
});
489
620
```
490
621
@@ -502,32 +633,29 @@ new Promise(function() {
502
633
});
503
634
```
504
635
505
-
Technically, when an error happens, the promise state becomes "rejected", and the execution should jump to the closest rejection handler. But there is no such handler in the examples above.
506
-
507
-
Usually that means that the code is bad. Indeed, how come that there's no error handling?
636
+
In theory, nothing should happen. In case of an error happens, the promise state becomes "rejected", and the execution should jump to the closest rejection handler. But there is no such handler in the examples above. So the error gets "stuck".
508
637
509
-
Most JavaScript engines track such situations and generate a global error in that case. In the browser we can catch it using `window.addEventListener('unhandledrejection')` as specified in the [HTML standard](https://html.spec.whatwg.org/multipage/webappapis.html#unhandled-promise-rejections):
638
+
In practice, that means that the code is bad. Indeed, how come that there's no error handling?
510
639
640
+
Most JavaScript engines track such situations and generate a global error in that case. In the browser we can catch it using the event `unhandledrejection`:
alert(event.promise); // [object Promise] - the promise that generated the error
516
647
alert(event.reason); // Error: Whoops! - the unhandled error object
517
648
});
649
+
*/!*
518
650
519
651
newPromise(function() {
520
652
thrownewError("Whoops!");
521
-
}).then(function() {
522
-
// ...something...
523
-
}).then(function() {
524
-
// ...something else...
525
-
}).then(function() {
526
-
// ...but no catch after it!
527
-
});
653
+
}); // no catch to handle the error
528
654
```
529
655
530
-
Now if an error has occured, and there's no `.catch`, the event `unhandledrejection` triggers, and our handler can do something with the exception. Once again, such situation is usually a programming error.
656
+
The event is the part of the [HTML standard](https://html.spec.whatwg.org/multipage/webappapis.html#unhandled-promise-rejections). Now if an error occurs, and there's no `.catch`, the `unhandledrejection` handler triggers: the `event` object has the information about the error, so we can do something with it.
657
+
658
+
Usually such errors are unrecoverable, so our best way out is to inform the user about the problem and probably report about the incident to the server.
531
659
532
660
In non-browser environments like Node.JS there are other similar ways to track unhandled errors.
533
661
@@ -549,6 +677,6 @@ The smaller picture of how handlers are called:
549
677
550
678
In the examples of error handling above the `.catch` was always the last in the chain. In practice though, not every promise chain has a `.catch`. Just like regular code is not always wrapped in `try..catch`.
551
679
552
-
We should place `.catch` exactly in the places where we want to handle errors and know how to handle them.
680
+
We should place `.catch` exactly in the places where we want to handle errors and know how to handle them. Using custom error classes can help to analyze errors and rethrow those that we can't handle.
553
681
554
-
For errors that are outside of that scope we should have the `unhandledrejection` event handler. Such unknown errors are usually unrecoverable, so all we should do is to inform the user and probably report to our server about the incident.
682
+
For errors that fall outside of our scope we should have the `unhandledrejection` event handler (for browsers, and analogs for other environments). Such unknown errors are usually unrecoverable, so all we should do is to inform the user and probably report to our server about the incident.
0 commit comments