minor
This commit is contained in:
parent
e80667391f
commit
8bd8e90488
5 changed files with 36 additions and 191 deletions
|
@ -209,9 +209,9 @@ function showMessage(from, text = anotherFunction()) {
|
|||
```
|
||||
|
||||
```smart header="Evaluation of default parameters"
|
||||
In JavaScript, a default parameter is evaluated every time the function is called without the respective parameter. In the example above, `anotherFunction()` is called every time `showMessage()` is called without the `text` parameter.
|
||||
In JavaScript, a default parameter is evaluated every time the function is called without the respective parameter.
|
||||
|
||||
This is in contrast to some other languages like Python, where any default parameters are evaluated only once during the initial interpretation.
|
||||
In the example above, `anotherFunction()` is called every time `showMessage()` is called without the `text` parameter.
|
||||
```
|
||||
|
||||
````smart header="Default parameters old-style"
|
||||
|
|
|
@ -331,4 +331,4 @@ We can immediately see a few benefits over the callback-based pattern:
|
|||
| Promises allow us to do things in the natural order. First, we run `loadScript(script)`, and `.then` we write what to do with the result. | We must have a `callback` function at our disposal when calling `loadScript(script, callback)`. In other words, we must know what to do with the result *before* `loadScript` is called. |
|
||||
| We can call `.then` on a Promise as many times as we want. Each time, we're adding a new "fan", a new subscribing function, to the "subscription list". More about this in the next chapter: [](info:promise-chaining). | There can be only one callback. |
|
||||
|
||||
So Promises give us better code flow and flexibility. But there's more. We'll see that in the next chapters.
|
||||
So promises give us better code flow and flexibility. But there's more. We'll see that in the next chapters.
|
||||
|
|
|
@ -48,24 +48,6 @@ The whole thing works, because a call to `promise.then` returns a promise, so th
|
|||
|
||||
When a handler returns a value, it becomes the result of that promise, so the next `.then` is called with it.
|
||||
|
||||
To make these words more clear, here's the start of the chain:
|
||||
|
||||
```js run
|
||||
new Promise(function(resolve, reject) {
|
||||
|
||||
setTimeout(() => resolve(1), 1000);
|
||||
|
||||
}).then(function(result) {
|
||||
|
||||
alert(result);
|
||||
return result * 2; // <-- (1)
|
||||
|
||||
}) // <-- (2)
|
||||
// .then…
|
||||
```
|
||||
|
||||
The value returned by `.then` is a promise, that's why we are able to add another `.then` at `(2)`. When the value is returned in `(1)`, that promise becomes resolved, so the next handler runs with the value.
|
||||
|
||||
**A classic newbie error: technically we can also add many `.then` to a single promise. This is not chaining.**
|
||||
|
||||
For example:
|
||||
|
@ -138,9 +120,9 @@ new Promise(function(resolve, reject) {
|
|||
});
|
||||
```
|
||||
|
||||
Here the first `.then` shows `1` and returns `new Promise(…)` in the line `(*)`. After one second it resolves, and the result (the argument of `resolve`, here it's `result*2`) is passed on to handler of the second `.then` in the line `(**)`. It shows `2` and does the same thing.
|
||||
Here the first `.then` shows `1` and returns `new Promise(…)` in the line `(*)`. After one second it resolves, and the result (the argument of `resolve`, here it's `result * 2`) is passed on to handler of the second `.then`. That handler is in the line `(**)`, it shows `2` and does the same thing.
|
||||
|
||||
So the output is again 1 -> 2 -> 4, but now with 1 second delay between `alert` calls.
|
||||
So the output is the same as in the previous example: 1 -> 2 -> 4, but now with 1 second delay between `alert` calls.
|
||||
|
||||
Returning promises allows us to build chains of asynchronous actions.
|
||||
|
||||
|
@ -184,7 +166,7 @@ Here each `loadScript` call returns a promise, and the next `.then` runs when it
|
|||
|
||||
We can add more asynchronous actions to the chain. Please note that code is still "flat", it grows down, not to the right. There are no signs of "pyramid of doom".
|
||||
|
||||
Please note that technically we can add `.then` directly to each `loadScript`, like this:
|
||||
Technically, we could add `.then` directly to each `loadScript`, like this:
|
||||
|
||||
```js run
|
||||
loadScript("/article/promise-chaining/one.js").then(script1 => {
|
||||
|
@ -207,7 +189,7 @@ Sometimes it's ok to write `.then` directly, because the nested function has acc
|
|||
|
||||
|
||||
````smart header="Thenables"
|
||||
To be precise, `.then` may return a so-called "thenable" object - an arbitrary object that has method `.then`, and it will be treated the same way as a promise.
|
||||
To be precise, a handler may return not exactly a promise, but a so-called "thenable" object - an arbitrary object that has method `.then`, and it will be treated the same way as a promise.
|
||||
|
||||
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`.
|
||||
|
||||
|
@ -227,7 +209,9 @@ class Thenable {
|
|||
|
||||
new Promise(resolve => resolve(1))
|
||||
.then(result => {
|
||||
*!*
|
||||
return new Thenable(result); // (*)
|
||||
*/!*
|
||||
})
|
||||
.then(alert); // shows 2 after 1000ms
|
||||
```
|
||||
|
@ -242,7 +226,7 @@ This feature allows to integrate custom objects with promise chains without havi
|
|||
|
||||
In frontend programming promises are often used for network requests. So let's see an extended example of that.
|
||||
|
||||
We'll use the [fetch](info:fetch) method to load the information about the user from the remote server. It has a lot of optional parameters covered in separate chapters, but the basic syntax is quite simple:
|
||||
We'll use the [fetch](info:fetch) method to load the information about the user from the remote server. It has a lot of optional parameters covered in [separate chapters](info:fetch), but the basic syntax is quite simple:
|
||||
|
||||
```js
|
||||
let promise = fetch(url);
|
||||
|
@ -259,7 +243,7 @@ fetch('/article/promise-chaining/user.json')
|
|||
// .then below runs when the remote server responds
|
||||
.then(function(response) {
|
||||
// response.text() returns a new promise that resolves with the full response text
|
||||
// when we finish downloading it
|
||||
// when it loads
|
||||
return response.text();
|
||||
})
|
||||
.then(function(text) {
|
||||
|
@ -276,7 +260,7 @@ We'll also use arrow functions for brevity:
|
|||
// same as above, but response.json() parses the remote content as JSON
|
||||
fetch('/article/promise-chaining/user.json')
|
||||
.then(response => response.json())
|
||||
.then(user => alert(user.name)); // iliakan
|
||||
.then(user => alert(user.name)); // iliakan, got user name
|
||||
```
|
||||
|
||||
Now let's do something with the loaded user.
|
||||
|
@ -317,7 +301,7 @@ fetch('/article/promise-chaining/user.json')
|
|||
.then(user => fetch(`https://api.github.com/users/${user.name}`))
|
||||
.then(response => response.json())
|
||||
*!*
|
||||
.then(githubUser => new Promise(function(resolve, reject) {
|
||||
.then(githubUser => new Promise(function(resolve, reject) { // (*)
|
||||
*/!*
|
||||
let img = document.createElement('img');
|
||||
img.src = githubUser.avatar_url;
|
||||
|
@ -327,7 +311,7 @@ fetch('/article/promise-chaining/user.json')
|
|||
setTimeout(() => {
|
||||
img.remove();
|
||||
*!*
|
||||
resolve(githubUser);
|
||||
resolve(githubUser); // (**)
|
||||
*/!*
|
||||
}, 3000);
|
||||
}))
|
||||
|
@ -335,9 +319,11 @@ fetch('/article/promise-chaining/user.json')
|
|||
.then(githubUser => alert(`Finished showing ${githubUser.name}`));
|
||||
```
|
||||
|
||||
Now right after `setTimeout` runs `img.remove()`, it calls `resolve(githubUser)`, thus passing the control to the next `.then` in the chain and passing forward the user data.
|
||||
That is, `.then` handler in the line `(*)` now returns `new Promise`, that becomes settled only after the call of `resolve(githubUser)` in `setTimeout` `(**)`.
|
||||
|
||||
As a rule, an asynchronous action should always return a promise.
|
||||
The next `.then` in chain will wait for that.
|
||||
|
||||
As a good rule, an asynchronous action should always return a promise.
|
||||
|
||||
That makes it possible to plan actions after it. Even if we don't plan to extend the chain now, we may need it later.
|
||||
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
|
||||
# Error handling with promises
|
||||
|
||||
Asynchronous actions may sometimes fail: in case of an error the corresponding promise becomes rejected. For instance, `fetch` fails if the remote server is not available. We can use `.catch` to handle errors (rejections).
|
||||
Promise chains are great at error handling. When a promise rejects, the control jumps to the closest rejection handler. That's very convenient in practice.
|
||||
|
||||
Promise chaining is great at that aspect. When a promise rejects, the control jumps to the closest rejection handler down the chain. That's very convenient in practice.
|
||||
|
||||
For instance, in the code below the URL is wrong (no such site) and `.catch` handles the error:
|
||||
For instance, in the code below the URL to `fetch` is wrong (no such site) and `.catch` handles the error:
|
||||
|
||||
```js run
|
||||
*!*
|
||||
|
@ -15,17 +13,9 @@ fetch('https://no-such-server.blabla') // rejects
|
|||
.catch(err => alert(err)) // TypeError: failed to fetch (the text may vary)
|
||||
```
|
||||
|
||||
Or, maybe, everything is all right with the site, but the response is not valid JSON:
|
||||
As you can see, the `.catch` doesn't have to be immediate. It may appear after one or maybe several `.then`.
|
||||
|
||||
```js run
|
||||
fetch('/') // fetch works fine now, the server responds with the HTML page
|
||||
*!*
|
||||
.then(response => response.json()) // rejects: the page is HTML, not a valid json
|
||||
*/!*
|
||||
.catch(err => alert(err)) // SyntaxError: Unexpected token < in JSON at position 0
|
||||
```
|
||||
|
||||
The easiest way to catch all errors is to append `.catch` to the end of chain:
|
||||
Or, maybe, everything is all right with the site, but the response is not valid JSON. The easiest way to catch all errors is to append `.catch` to the end of chain:
|
||||
|
||||
```js run
|
||||
fetch('/article/promise-chaining/user.json')
|
||||
|
@ -48,7 +38,7 @@ fetch('/article/promise-chaining/user.json')
|
|||
*/!*
|
||||
```
|
||||
|
||||
Normally, `.catch` doesn't trigger at all, because there are no errors. But if any of the promises above rejects (a network problem or invalid json or whatever), then it would catch it.
|
||||
Normally, such `.catch` doesn't trigger at all. But if any of the promises above rejects (a network problem or invalid json or whatever), then it would catch it.
|
||||
|
||||
## Implicit try..catch
|
||||
|
||||
|
@ -74,9 +64,9 @@ new Promise((resolve, reject) => {
|
|||
}).catch(alert); // Error: Whoops!
|
||||
```
|
||||
|
||||
The "invisible `try..catch`" around the executor automatically catches the error and treats it as a rejection.
|
||||
The "invisible `try..catch`" around the executor automatically catches the error and turns it into rejected promise.
|
||||
|
||||
This happens not only in the executor, but in its handlers as well. If we `throw` inside a `.then` handler, that means a rejected promise, so the control jumps to the nearest error handler.
|
||||
This happens not only in the executor function, but in its handlers as well. If we `throw` inside a `.then` handler, that means a rejected promise, so the control jumps to the nearest error handler.
|
||||
|
||||
Here's an example:
|
||||
|
||||
|
@ -106,7 +96,7 @@ The final `.catch` not only catches explicit rejections, but also occasional err
|
|||
|
||||
## Rethrowing
|
||||
|
||||
As we already noticed, `.catch` behaves like `try..catch`. We may have as many `.then` handlers as we want, and then use a single `.catch` at the end to handle errors in all of them.
|
||||
As we already noticed, `.catch` at the end of the chain is similar to `try..catch`. We may have as many `.then` handlers as we want, and then use a single `.catch` at the end to handle errors in all of them.
|
||||
|
||||
In a regular `try..catch` we can analyze the error and maybe rethrow it if can't handle. The same thing is possible for promises.
|
||||
|
||||
|
@ -150,7 +140,7 @@ new Promise((resolve, reject) => {
|
|||
}
|
||||
|
||||
}).then(function() {
|
||||
/* never runs here */
|
||||
/* doesn't runs here */
|
||||
}).catch(error => { // (**)
|
||||
|
||||
alert(`The unknown error has occurred: ${error}`);
|
||||
|
@ -159,101 +149,11 @@ new Promise((resolve, reject) => {
|
|||
});
|
||||
```
|
||||
|
||||
Then the execution jumps from the first `.catch` `(*)` to the next one `(**)` down the chain.
|
||||
|
||||
In the section below we'll see a practical example of rethrowing.
|
||||
|
||||
## Fetch error handling example
|
||||
|
||||
Let's improve error handling for the user-loading example.
|
||||
|
||||
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.
|
||||
|
||||
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 `(**)`?
|
||||
|
||||
```js run
|
||||
fetch('no-such-user.json') // (*)
|
||||
.then(response => response.json())
|
||||
.then(user => fetch(`https://api.github.com/users/${user.name}`)) // (**)
|
||||
.then(response => response.json())
|
||||
.catch(alert); // SyntaxError: Unexpected token < in JSON at position 0
|
||||
// ...
|
||||
```
|
||||
|
||||
|
||||
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.
|
||||
|
||||
That's not good, because the error just falls through the chain, without details: what failed and where.
|
||||
|
||||
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.
|
||||
|
||||
```js run
|
||||
class HttpError extends Error { // (1)
|
||||
constructor(response) {
|
||||
super(`${response.status} for ${response.url}`);
|
||||
this.name = 'HttpError';
|
||||
this.response = response;
|
||||
}
|
||||
}
|
||||
|
||||
function loadJson(url) { // (2)
|
||||
return fetch(url)
|
||||
.then(response => {
|
||||
if (response.status == 200) {
|
||||
return response.json();
|
||||
} else {
|
||||
throw new HttpError(response);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
loadJson('no-such-user.json') // (3)
|
||||
.catch(alert); // HttpError: 404 for .../no-such-user.json
|
||||
```
|
||||
|
||||
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 `response` object and saves it in the error. So error-handling code will be able to access the response.
|
||||
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.
|
||||
3. Now `alert` shows a more helpful descriptive message.
|
||||
|
||||
The great thing about having our own class for errors is that we can easily check for it in error-handling code using `instanceof`.
|
||||
|
||||
For instance, we can make a request, and then if we get 404 -- ask the user to modify the information.
|
||||
|
||||
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:
|
||||
|
||||
```js run
|
||||
function demoGithubUser() {
|
||||
let name = prompt("Enter a name?", "iliakan");
|
||||
|
||||
return loadJson(`https://api.github.com/users/${name}`)
|
||||
.then(user => {
|
||||
alert(`Full name: ${user.name}.`);
|
||||
return user;
|
||||
})
|
||||
.catch(err => {
|
||||
*!*
|
||||
if (err instanceof HttpError && err.response.status == 404) {
|
||||
*/!*
|
||||
alert("No such user, please reenter.");
|
||||
return demoGithubUser();
|
||||
} else {
|
||||
throw err; // (*)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
demoGithubUser();
|
||||
```
|
||||
|
||||
Please note: `.catch` here catches all errors, but it "knows how to handle" only `HttpError 404`. In that particular case it means that there's no such user, and `.catch` just retries in that case.
|
||||
|
||||
For other errors, it has no idea what could go wrong. Maybe a programming error or something. So it just rethrows it in the line `(*)`.
|
||||
The execution jumps from the first `.catch` `(*)` to the next one `(**)` down the chain.
|
||||
|
||||
## Unhandled rejections
|
||||
|
||||
What happens when an error is not handled? For instance, after the rethrow `(*)` in the example above.
|
||||
|
||||
Or we could just forget to append an error handler to the end of the chain, like here:
|
||||
What happens when an error is not handled? For instance, we forgot to append `.catch` to the end of the chain, like here:
|
||||
|
||||
```js untrusted run refresh
|
||||
new Promise(function() {
|
||||
|
@ -264,13 +164,13 @@ new Promise(function() {
|
|||
}); // without .catch at the end!
|
||||
```
|
||||
|
||||
In case of an error, 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". There's no code to handle it.
|
||||
In case of an error, the promise becomes rejected, and the execution should jump to the closest rejection handler. But there is none. So the error gets "stuck". There's no code to handle it.
|
||||
|
||||
In practice, just like with a regular unhandled errors, it means that something has terribly gone wrong.
|
||||
In practice, just like with a regular unhandled errors in code, it means that something has terribly gone wrong.
|
||||
|
||||
What happens when a regular error occurs and is not caught by `try..catch`? The script dies. Similar thing happens with unhandled promise rejections.
|
||||
What happens when a regular error occurs and is not caught by `try..catch`? The script dies with a message in console. Similar thing happens with unhandled promise rejections.
|
||||
|
||||
The JavaScript engine tracks such rejections and generates a global error in that case. You can see it in the console if you run the example above.
|
||||
JavaScript engine tracks such rejections and generates a global error in that case. You can see it in the console if you run the example above.
|
||||
|
||||
In the browser we can catch such errors using the event `unhandledrejection`:
|
||||
|
||||
|
@ -294,52 +194,11 @@ If an error occurs, and there's no `.catch`, the `unhandledrejection` handler tr
|
|||
|
||||
Usually such errors are unrecoverable, so our best way out is to inform the user about the problem and probably report the incident to the server.
|
||||
|
||||
In non-browser environments like Node.js there are other similar ways to track unhandled errors.
|
||||
|
||||
In non-browser environments like Node.js there are other ways to track unhandled errors.
|
||||
|
||||
## Summary
|
||||
|
||||
- `.catch` handles promise rejections of all kinds: be it a `reject()` call, or an error thrown in a handler.
|
||||
- We should place `.catch` exactly in places where we want to handle errors and know how to handle them. The handler should analyze errors (custom error classes help) and rethrow unknown ones.
|
||||
- `.catch` handles errors in promises of all kinds: be it a `reject()` call, or an error thrown in a handler.
|
||||
- We should place `.catch` exactly in places where we want to handle errors and know how to handle them. The handler should analyze errors (custom error classes help) and rethrow unknown ones (maybe they are programming mistakes).
|
||||
- It's ok not to use `.catch` at all, if there's no way to recover from an error.
|
||||
- In any case we should have the `unhandledrejection` event handler (for browsers, and analogs for other environments), to track unhandled errors and inform the user (and probably our server) about the them, so that our app never "just dies".
|
||||
|
||||
And finally, if we have load-indication, then `.finally` is a great handler to stop it when the fetch is complete:
|
||||
|
||||
```js run
|
||||
function demoGithubUser() {
|
||||
let name = prompt("Enter a name?", "iliakan");
|
||||
|
||||
*!*
|
||||
document.body.style.opacity = 0.3; // (1) start the indication
|
||||
*/!*
|
||||
|
||||
return loadJson(`https://api.github.com/users/${name}`)
|
||||
*!*
|
||||
.finally(() => { // (2) stop the indication
|
||||
document.body.style.opacity = '';
|
||||
return new Promise(resolve => setTimeout(resolve)); // (*)
|
||||
})
|
||||
*/!*
|
||||
.then(user => {
|
||||
alert(`Full name: ${user.name}.`);
|
||||
return user;
|
||||
})
|
||||
.catch(err => {
|
||||
if (err instanceof HttpError && err.response.status == 404) {
|
||||
alert("No such user, please reenter.");
|
||||
return demoGithubUser();
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
demoGithubUser();
|
||||
```
|
||||
|
||||
Here on the line `(1)` we indicate loading by dimming the document. The method doesn't matter, could use any type of indication instead.
|
||||
|
||||
When the promise is settled, be it a successful fetch or an error, `finally` triggers at the line `(2)` and stops the indication.
|
||||
|
||||
There's a little browser trick `(*)` with returning a zero-timeout promise from `finally`. That's because some browsers (like Chrome) need "a bit time" outside promise handlers to paint document changes. So it ensures that the indication is visually stopped before going further on the chain.
|
||||
|
|
BIN
figures.sketch
BIN
figures.sketch
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue