Skip to content

Commit 0ad6ba0

Browse files
committed
better async
1 parent e456067 commit 0ad6ba0

File tree

14 files changed

+584
-55
lines changed

14 files changed

+584
-55
lines changed

8-async/02-promise-basics/article.md

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,8 @@ let promise = new Promise(function(resolve, reject) {
113113
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.
114114
````
115115
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.
118118
```
119119
120120
## Consumers: ".then" and ".catch"
@@ -200,10 +200,25 @@ If a promise is pending, `.then/catch` handlers wait for the result. Otherwise,
200200
// an immediately resolved promise
201201
let promise = new Promise(resolve => resolve("done!"));
202202
203-
promise.then(alert); // done! (without a delay)
203+
promise.then(alert); // done! (shows up right now)
204204
```
205205
206206
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.
207222
````
208223

209224
Now let's see more practical examples to see how promises can help us in writing asynchronous code.

8-async/03-promise-chaining/article.md

Lines changed: 157 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -189,27 +189,63 @@ This code does the same: loads 3 scripts in sequence. But it "grows to the right
189189

190190
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.
191191

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
210+
setTimeout(() => resolve(this.result * 2), delay);
211+
}
212+
};
213+
214+
new Promise(resolve => resolve(1))
215+
.then(result => {
216+
return new Thenable(result, 1000); // (*)
217+
})
218+
.then(alert); // shows 2 after 1000ms
219+
```
220+
221+
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+
192227
## Bigger example: fetch
193228

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.
195230

196231
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:
197232

198233
```js
199234
let promise = fetch(url);
200235
```
201236

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.
203238

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.
205240

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:
207242

208243
```js run
209244
fetch('/article/promise-chaining/user.json')
210-
// .then runs when the remote server responds
245+
// .then below runs when the remote server responds
211246
.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
213249
return response.text();
214250
})
215251
.then(function(text) {
@@ -223,12 +259,15 @@ There is also a method `response.json()` that reads the remote data and parses i
223259
We'll also use arrow functions for brevity:
224260

225261
```js run
262+
// same as above, but response.json() parses the remote content as JSON
226263
fetch('/article/promise-chaining/user.json')
227264
.then(response => response.json())
228265
.then(user => alert(user.name)); // iliakan
229266
```
230267

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:
232271

233272
```js run
234273
// 1. Make a request for user.json
@@ -250,9 +289,9 @@ fetch('/article/promise-chaining/user.json')
250289
});
251290
```
252291

253-
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.
254293

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.
256295

257296
To make the chain extendable, we need to return a promise that resolves when the avatar finishes showing.
258297

@@ -265,24 +304,28 @@ fetch('/article/promise-chaining/user.json')
265304
.then(response => response.json())
266305
*!*
267306
.then(githubUser => new Promise(function(resolve, reject) {
307+
*/!*
268308
let img = document.createElement('img');
269309
img.src = githubUser.avatar_url;
270310
img.className = "promise-avatar-example";
271311
document.body.append(img);
272312

273313
setTimeout(() => {
274314
img.remove();
315+
*!*
275316
resolve(githubUser);
317+
*/!*
276318
}, 3000);
277319
}))
278-
*/!*
279320
// triggers after 3 seconds
280321
.then(githubUser => alert(`Finished showing ${githubUser.name}`));
281322
```
282323

283324
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.
284325

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.
286329

287330
Finally, we can split the code into reusable functions:
288331

@@ -311,6 +354,7 @@ function showAvatar(githubUser) {
311354
});
312355
}
313356

357+
// Use them:
314358
loadJson('/article/promise-chaining/user.json')
315359
.then(user => loadGithubUser(user.name))
316360
.then(showAvatar)
@@ -476,15 +520,102 @@ new Promise(function(resolve, reject) {
476520

477521
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 `(**)`.
478522

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 `(**)`?
532+
533+
```js run
534+
fetch('no-such-user.json') // (*)
535+
.then(response => response.json())
536+
.then(user => fetch(`https://api.github.com/users/${user.name}`)) // (**)
537+
.then(response => response.json())
538+
.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.
480548

481-
...But what if we forget to append an error handler to the end of the chain?
549+
```js run
550+
class HttpError extends Error { // (1)
551+
constructor(response) {
552+
super(`${response.status} for ${response.url}`);
553+
this.name = 'HttpError';
554+
this.response = response;
555+
}
556+
}
557+
558+
function loadJson(url) { // (2)
559+
return fetch(url)
560+
.then(response => {
561+
if (response.status == 200) {
562+
return response.json();
563+
} else {
564+
throw new HttpError(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.
482576

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:
582+
583+
```js run
584+
function demoGithubUser() {
585+
let name = prompt("Enter a name?", "iliakan");
586+
587+
return loadJson(`https://api.github.com/users/${name}`)
588+
.then(user => {
589+
alert(`Full name: ${user.name}.`); // (1)
590+
return user;
591+
})
592+
.catch(err => {
593+
*!*
594+
if (err instanceof HttpError && err.response.status == 404) { // (2)
595+
*/!*
596+
alert("No such user, please reenter.");
597+
return demoGithubUser();
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:
484615

485616
```js untrusted run refresh
486617
new Promise(function() {
487-
errorHappened(); // Error here (no such function)
618+
noSuchFunction(); // Error here (no such function)
488619
});
489620
```
490621

@@ -502,32 +633,29 @@ new Promise(function() {
502633
});
503634
```
504635

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".
508637

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?
510639

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`:
511641

512642
```js run
643+
*!*
513644
window.addEventListener('unhandledrejection', function(event) {
514645
// the event object has two special properties:
515646
alert(event.promise); // [object Promise] - the promise that generated the error
516647
alert(event.reason); // Error: Whoops! - the unhandled error object
517648
});
649+
*/!*
518650

519651
new Promise(function() {
520652
throw new Error("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
528654
```
529655

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.
531659

532660
In non-browser environments like Node.JS there are other similar ways to track unhandled errors.
533661

@@ -549,6 +677,6 @@ The smaller picture of how handlers are called:
549677

550678
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`.
551679

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.
553681

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.

8-async/03-promise-chaining/head.html

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,25 @@
1010
document.head.append(script);
1111
});
1212
}
13+
14+
class HttpError extends Error {
15+
constructor(response) {
16+
super(`${response.status} for ${response.url}`);
17+
this.name = 'HttpError';
18+
this.response = response;
19+
}
20+
}
21+
22+
function loadJson(url) {
23+
return fetch(url)
24+
.then(response => {
25+
if (response.status == 200) {
26+
return response.json();
27+
} else {
28+
throw new HttpError(response);
29+
}
30+
})
31+
}
1332
</script>
1433

1534
<style>

0 commit comments

Comments
 (0)