diff --git a/1-js/04-object-basics/03-symbol/article.md b/1-js/04-object-basics/03-symbol/article.md index b3dfe9e1..9d39b36d 100644 --- a/1-js/04-object-basics/03-symbol/article.md +++ b/1-js/04-object-basics/03-symbol/article.md @@ -74,7 +74,9 @@ alert(id.description); // id Symbols allow us to create "hidden" properties of an object, that no other part of code can occasionally access or overwrite. -For instance, if we'd like to add an "identifier" to the object `user`, we can use a symbol as a key for it: +For instance, if we're working with `user` objects, that come from a third-party code and don't have any `id` field. We'd like to add identifiers to them. + +Let's use a symbol key for it: ```js run let user = { name: "John" }; @@ -86,9 +88,9 @@ alert( user[id] ); // we can access the data using the symbol as the key What's the benefit of using `Symbol("id")` over a string `"id"`? -Let's make the example a bit deeper to see that. +As `user` objects come from another code, and that code works with them, we shouldn't just add any fields to it. That's unsafe. -Imagine that another script wants to have its own identifier inside `user`, for its own purposes. That may be another JavaScript library, so thes scripts are completely unaware of each other. +Also, imagine that another script wants to have its own identifier inside `user`, for its own purposes. That may be another JavaScript library, so that the scripts are completely unaware of each other. Then that script can create its own `Symbol("id")`, like this: diff --git a/1-js/11-async/05-promise-api/01-promise-errors-as-results/solution.md b/1-js/11-async/05-promise-api/01-promise-errors-as-results/solution.md deleted file mode 100644 index e50a5d49..00000000 --- a/1-js/11-async/05-promise-api/01-promise-errors-as-results/solution.md +++ /dev/null @@ -1,44 +0,0 @@ -The solution is actually pretty simple. - -Take a look at this: - -```js -Promise.all( - fetch('https://api.github.com/users/iliakan'), - fetch('https://api.github.com/users/remy'), - fetch('http://no-such-url') -) -``` - -Here we have an array of `fetch(...)` promises that goes to `Promise.all`. - -We can't change the way `Promise.all` works: if any promise rejects with an error, then `Promise.all` as a whole rejects with it. So we need to prevent any error from occurring. Instead, if a `fetch` error happens, we need to treat it as a "normal" result. - -Here's how: - -```js -Promise.all( - fetch('https://api.github.com/users/iliakan').catch(err => err), - fetch('https://api.github.com/users/remy').catch(err => err), - fetch('http://no-such-url').catch(err => err) -) -``` - -In other words, the `.catch` takes an error for all of the promises and returns it normally. By the rules of how promises work, if a `.then/catch` handler returns a value (doesn't matter if it's an error object or something else), then the execution continues the "normal" flow. - -So the `.catch` returns the error as a "normal" result into the outer `Promise.all`. - -This code: -```js -Promise.all( - urls.map(url => fetch(url)) -) -``` - -Can be rewritten as: - -```js -Promise.all( - urls.map(url => fetch(url).catch(err => err)) -) -``` diff --git a/1-js/11-async/05-promise-api/01-promise-errors-as-results/solution.view/index.html b/1-js/11-async/05-promise-api/01-promise-errors-as-results/solution.view/index.html deleted file mode 100644 index 209e6064..00000000 --- a/1-js/11-async/05-promise-api/01-promise-errors-as-results/solution.view/index.html +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - diff --git a/1-js/11-async/05-promise-api/01-promise-errors-as-results/source.view/index.html b/1-js/11-async/05-promise-api/01-promise-errors-as-results/source.view/index.html deleted file mode 100644 index c760ad8c..00000000 --- a/1-js/11-async/05-promise-api/01-promise-errors-as-results/source.view/index.html +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - diff --git a/1-js/11-async/05-promise-api/01-promise-errors-as-results/task.md b/1-js/11-async/05-promise-api/01-promise-errors-as-results/task.md deleted file mode 100644 index c37ad56e..00000000 --- a/1-js/11-async/05-promise-api/01-promise-errors-as-results/task.md +++ /dev/null @@ -1,48 +0,0 @@ -# Fault-tolerant Promise.all - -We'd like to fetch multiple URLs in parallel. - -Here's the code to do that: - -```js run -let urls = [ - 'https://api.github.com/users/iliakan', - 'https://api.github.com/users/remy', - 'https://api.github.com/users/jeresig' -]; - -Promise.all(urls.map(url => fetch(url))) - // for each response show its status - .then(responses => { // (*) - for(let response of responses) { - alert(`${response.url}: ${response.status}`); - } - }); -``` - -The problem is that if any of requests fails, then `Promise.all` rejects with the error, and we lose the results of all the other requests. - -That's not good. - -Modify the code so that the array `responses` in the line `(*)` would include the response objects for successful fetches and error objects for failed ones. - -For instance, if one of the URLs is bad, then it should be like: - -```js -let urls = [ - 'https://api.github.com/users/iliakan', - 'https://api.github.com/users/remy', - 'http://no-such-url' -]; - -Promise.all(...) // your code to fetch URLs... - // ...and pass fetch errors as members of the resulting array... - .then(responses => { - // 3 urls => 3 array members - alert(responses[0].status); // 200 - alert(responses[1].status); // 200 - alert(responses[2]); // TypeError: failed to fetch (text may vary) - }); -``` - -P.S. In this task you don't have to load the full response using `response.text()` or `response.json()`. Just handle fetch errors the right way. diff --git a/1-js/11-async/05-promise-api/02-promise-errors-as-results-2/solution.md b/1-js/11-async/05-promise-api/02-promise-errors-as-results-2/solution.md deleted file mode 100644 index e69de29b..00000000 diff --git a/1-js/11-async/05-promise-api/02-promise-errors-as-results-2/solution.view/index.html b/1-js/11-async/05-promise-api/02-promise-errors-as-results-2/solution.view/index.html deleted file mode 100644 index 744efd2b..00000000 --- a/1-js/11-async/05-promise-api/02-promise-errors-as-results-2/solution.view/index.html +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - diff --git a/1-js/11-async/05-promise-api/02-promise-errors-as-results-2/source.view/index.html b/1-js/11-async/05-promise-api/02-promise-errors-as-results-2/source.view/index.html deleted file mode 100644 index adb86d41..00000000 --- a/1-js/11-async/05-promise-api/02-promise-errors-as-results-2/source.view/index.html +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - diff --git a/1-js/11-async/05-promise-api/02-promise-errors-as-results-2/task.md b/1-js/11-async/05-promise-api/02-promise-errors-as-results-2/task.md deleted file mode 100644 index d28cf87e..00000000 --- a/1-js/11-async/05-promise-api/02-promise-errors-as-results-2/task.md +++ /dev/null @@ -1,34 +0,0 @@ -# Fault-tolerant fetch with JSON - -Improve the solution of the previous task . Now we need not just to call `fetch`, but to load the JSON objects from the given URLs. - -Here's the example code to do that: - -```js run -let urls = [ - 'https://api.github.com/users/iliakan', - 'https://api.github.com/users/remy', - 'https://api.github.com/users/jeresig' -]; - -// make fetch requests -Promise.all(urls.map(url => fetch(url))) - // map each response to response.json() - .then(responses => Promise.all( - responses.map(r => r.json()) - )) - // show name of each user - .then(users => { // (*) - for(let user of users) { - alert(user.name); - } - }); -``` - -The problem is that if any of requests fails, then `Promise.all` rejects with the error, and we lose results of all the other requests. So the code above is not fault-tolerant, just like the one in the previous task. - -Modify the code so that the array in the line `(*)` would include parsed JSON for successful requests and error for errored ones. - -Please note that the error may occur both in `fetch` (if the network request fails) and in `response.json()` (if the response is invalid JSON). In both cases the error should become a member of the results object. - -The sandbox has both of these cases. diff --git a/1-js/11-async/05-promise-api/article.md b/1-js/11-async/05-promise-api/article.md index d049f3b0..ed071910 100644 --- a/1-js/11-async/05-promise-api/article.md +++ b/1-js/11-async/05-promise-api/article.md @@ -1,6 +1,6 @@ # Promise API -There are 4 static methods in the `Promise` class. We'll quickly cover their use cases here. +There are 5 static methods in the `Promise` class. We'll quickly cover their use cases here. ## Promise.resolve @@ -102,7 +102,7 @@ let urls = [ 'https://api.github.com/users/jeresig' ]; -// map every url to the promise fetch(github url) +// map every url to the promise of the fetch let requests = urls.map(url => fetch(url)); // Promise.all waits until all jobs are resolved @@ -112,7 +112,7 @@ Promise.all(requests) )); ``` -A more real-life example with fetching user information for an array of github users by their names (or we could fetch an array of goods by their ids, the logic is same): +A bigger example with fetching user information for an array of github users by their names (or we could fetch an array of goods by their ids, the logic is same): ```js run let names = ['iliakan', 'remy', 'jeresig']; @@ -134,7 +134,7 @@ Promise.all(requests) .then(users => users.forEach(user => alert(user.name))); ``` -If any of the promises is rejected, `Promise.all` immediately rejects with that error. +**If any of the promises is rejected, `Promise.all` immediately rejects with that error.** For instance: @@ -150,9 +150,13 @@ Promise.all([ Here the second promise rejects in two seconds. That leads to immediate rejection of `Promise.all`, so `.catch` executes: the rejection error becomes the outcome of the whole `Promise.all`. -The important detail is that promises provide no way to "cancel" or "abort" their execution. So other promises continue to execute, and then eventually settle, but all their results are ignored. +```warn header="In case of an error, other promises are ignored" +If one promise rejects, `Promise.all` immediately rejects, completely forgetting about the other ones in the list. Their results are ignored. -There are ways to avoid this: we can either write additional code to `clearTimeout` (or otherwise cancel) the promises in case of an error, or we can make errors show up as members in the resulting array (see the task below this chapter about it). +For example, if there are multiple `fetch` calls, like in the example above, and one fails, other ones will still continue to execute, but `Promise.all` don't watch them any more. They will probably settle, but the result will be ignored. + +`Promise.all` does nothing to cancel them, as there's no concept of "cancellation" in promises. In [another chapter](fetch-abort) we'll cover `AbortController` that aims to help with that, but it's not a part of the Promise API. +``` ````smart header="`Promise.all(...)` allows non-promise items in `iterable`" Normally, `Promise.all(...)` accepts an iterable (in most cases an array) of promises. But if any of those objects is not a promise, it's wrapped in `Promise.resolve`. @@ -173,6 +177,84 @@ So we are able to pass non-promise values to `Promise.all` where convenient. ```` +## Promise.allSettled + +[recent browser="new"] + +`Promise.all` rejects as a whole if any promise rejects. That's good in cases, when we need *all* results to go on: + +```js +Promise.all([ + fetch('/template.html'), + fetch('/style.css'), + fetch('/data.json') +]).then(render); // render method needs them all +``` + +`Promise.allSettled` waits for all promises to settle: even if one rejects, it waits for the others. The resulting array has: + +- `{status:"fulfilled", value:result}` for successful responses, +- `{status:"rejected", reason:error}` for errors. + +For example, we'd like to fetch the information about multiple users. Even if one request fails, we're interested in the others. + +Let's use `Promise.allSettled`: + +```js run +let urls = [ + 'https://api.github.com/users/iliakan', + 'https://api.github.com/users/remy', + 'https://no-such-url' +]; + +Promise.allSettled(urls.map(url => fetch(url))) + .then(results => { // (*) + results.forEach((result, num) => { + if (result.status == "fulfilled") { + alert(`${urls[num]}: ${result.value.status}`); + } + if (result.status == "rejected") { + alert(`${urls[num]}: ${result.reason}`); + } + }); + }); +``` + +The `results` in the line `(*)` above will be: +```js +[ + {status: 'fulfilled', value: ...response...}, + {status: 'fulfilled', value: ...response...}, + {status: 'rejected', reason: ...error object...} +] +``` + +So, for each promise we get its status and `value/reason`. + +### Polyfill + +If the browser doesn't support `Promise.allSettled`, it's easy to polyfill: + +```js +if(!Promise.allSettled) { + Promise.allSettled = function(promises) { + return Promise.all(promises.map(p => Promise.resolve(p).then(v => ({ + state: 'fulfilled', + value: v, + }), r => ({ + state: 'rejected', + reason: r, + })))); + }; +} +``` + +In this code, `promises.map` takes input values, turns into promises (just in case a non-promise was passed) with `p => Promise.resolve(p)`, and then adds `.then` handler to it. + +That handler turns a successful result `v` into `{state:'fulfilled', value:v}`, and an error `r` into `{state:'rejected', reason:r}`. That's exactly the format of `Promise.allSettled`. + +Then we can use `Promise.allSettled` to get the results or *all* given promises, even if some of them reject. + ## Promise.race Similar to `Promise.all`, it takes an iterable of promises, but instead of waiting for all of them to finish, it waits for the first result (or error), and goes on with it. @@ -197,11 +279,14 @@ So, the first result/error becomes the result of the whole `Promise.race`. After ## Summary -There are 4 static methods of `Promise` class: +There are 5 static methods of `Promise` class: 1. `Promise.resolve(value)` -- makes a resolved promise with the given value. 2. `Promise.reject(error)` -- makes a rejected promise with the given error. 3. `Promise.all(promises)` -- waits for all promises to resolve and returns an array of their results. If any of the given promises rejects, then it becomes the error of `Promise.all`, and all other results are ignored. -4. `Promise.race(promises)` -- waits for the first promise to settle, and its result/error becomes the outcome. +4. `Promise.allSettled(promises)` (a new method) -- waits for all promises to resolve or reject and returns an array of their results as object with: + - `state`: `'fulfilled'` or `'rejected'` + - `value` (if fulfilled) or `reason` (if rejected). +5. `Promise.race(promises)` -- waits for the first promise to settle, and its result/error becomes the outcome. -Of these four, `Promise.all` is the most common in practice. +Of these five, `Promise.all/allSettled` are the most common in practice. diff --git a/1-js/11-async/08-async-await/02-rewrite-async-2/task.md b/1-js/11-async/08-async-await/02-rewrite-async-2/task.md index 94af2e57..a5c1c03a 100644 --- a/1-js/11-async/08-async-await/02-rewrite-async-2/task.md +++ b/1-js/11-async/08-async-await/02-rewrite-async-2/task.md @@ -1,5 +1,5 @@ -# Rewrite "rethrow" async/await +# Rewrite "rethrow" with async/await Below you can find the "rethrow" example from the chapter . Rewrite it using `async/await` instead of `.then/catch`. diff --git a/5-network/01-fetch-basics/01-fetch-users/_js.view/solution.js b/5-network/01-fetch-basics/01-fetch-users/_js.view/solution.js new file mode 100644 index 00000000..da448b47 --- /dev/null +++ b/5-network/01-fetch-basics/01-fetch-users/_js.view/solution.js @@ -0,0 +1,24 @@ + +async function getUsers(names) { + let jobs = []; + + for(let name of names) { + let job = fetch(`https://api.github.com/users/${name}`).then( + successResponse => { + if (successResponse.status != 200) { + return null; + } else { + return successResponse.json(); + } + }, + failResponse => { + return null; + } + ); + jobs.push(job); + } + + let results = await Promise.all(jobs); + + return results; +} diff --git a/5-network/01-fetch-basics/01-fetch-users/_js.view/source.js b/5-network/01-fetch-basics/01-fetch-users/_js.view/source.js new file mode 100644 index 00000000..0c62e7bb --- /dev/null +++ b/5-network/01-fetch-basics/01-fetch-users/_js.view/source.js @@ -0,0 +1,4 @@ + +async function getUsers(names) { + /* your code */ +} diff --git a/5-network/01-fetch-basics/01-fetch-users/_js.view/test.js b/5-network/01-fetch-basics/01-fetch-users/_js.view/test.js new file mode 100644 index 00000000..aad2f60f --- /dev/null +++ b/5-network/01-fetch-basics/01-fetch-users/_js.view/test.js @@ -0,0 +1,10 @@ +describe("getUsers", function() { + + it("gets users from GitHub", async function() { + let users = getUsers(['iliakan', 'remy', 'no.such.users']); + assert.equal(users[0].login, 'iliakan'); + assert.equal(users[1].login, 'remy'); + assert.equal(users[2], null); + }); + +}); diff --git a/5-network/01-fetch-basics/01-fetch-users/solution.md b/5-network/01-fetch-basics/01-fetch-users/solution.md new file mode 100644 index 00000000..6ce50451 --- /dev/null +++ b/5-network/01-fetch-basics/01-fetch-users/solution.md @@ -0,0 +1,41 @@ + +To fetch a user we need: + +1. `fetch('https://api.github.com/users/USERNAME')`. +2. If the response has status `200`, call `.json()` to read the JS object. + +If a `fetch` fails, or the response has non-200 status, we just return `null` in the resulting arrray. + +So here's the code: + +```js +async function getUsers(names) { + let jobs = []; + + for(let name of names) { + let job = fetch(`https://api.github.com/users/${name}`).then( + successResponse => { + if (successResponse.status != 200) { + return null; + } else { + return successResponse.json(); + } + }, + failResponse => { + return null; + } + ); + jobs.push(job); + } + + let results = await Promise.all(jobs); + + return results; +} +``` + +Please note: `.then` call is attached directly to `fetch`, so that when we have the response, it doesn't wait for other fetches, but starts to read `.json()` immediately. + +If we used `await Promise.all(names.map(name => fetch(...)))`, and call `.json()` on the results, then it would wait for all fetches to respond. By adding `.json()` directly to each `fetch`, we ensure that individual fetches start reading data as JSON without waiting for each other. + +That's an example of how low-level `Promise` API can still be useful even if we mainly use `async/await`. diff --git a/5-network/01-fetch-basics/01-fetch-users/task.md b/5-network/01-fetch-basics/01-fetch-users/task.md new file mode 100644 index 00000000..b820141a --- /dev/null +++ b/5-network/01-fetch-basics/01-fetch-users/task.md @@ -0,0 +1,12 @@ +# Fetch users from GitHub + +Create an async function `getUsers(names)`, that gets an array of GitHub user names, fetches them from GitHub and returns an array of GitHub users instead. + +The GitHub url with user informaiton is: `https://api.github.com/users/USERNAME`. + +There's a test example in the sandbox. + +Important details: + +1. There should be one `fetch` request per user. And requests shouldn't wait for each other. So that the data arrives as soon as possible. +2. If a request fails, or if there's no such user, the function should return `null` in the resulting array.