Publish async/await!

This commit is contained in:
Ilya Kantor 2017-05-25 11:12:41 +03:00
parent fb5f39e4f4
commit 787d58a83f
10 changed files with 253 additions and 45 deletions

View file

@ -637,7 +637,9 @@ In theory, nothing should happen. In case of an error happens, the promise state
In practice, that means that the code is bad. Indeed, how come that there's no error handling?
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`:
Most JavaScript engines track such situations and generate a global error in that case. We can see it in the console.
In the browser we can catch it using the event `unhandledrejection`:
```js run
*!*

View file

@ -71,29 +71,48 @@ The syntax is:
let promise = Promise.all(iterable);
```
It takes an `iterable` object with promises, for instance an array and returns a new promise that resolves with when all of them are settled and has an array of results.
It takes an `iterable` object with promises, technically it can be any iterable, but usually it's an array, and returns a new promise. The new promise resolves with when all of them are settled and has an array of their results.
For instance, the `Promise.all` below settles after 3 seconds, and the result is an array `[1, 2, 3]`:
For instance, the `Promise.all` below settles after 3 seconds, and then its result is an array `[1, 2, 3]`:
```js run
Promise.all([
new Promise((resolve, reject) => setTimeout(() => resolve(1), 3000)),
new Promise((resolve, reject) => setTimeout(() => resolve(2), 2000)),
new Promise((resolve, reject) => setTimeout(() => resolve(3), 1000))
]).then(alert); // 1,2,3 after all promises are ready
new Promise((resolve, reject) => setTimeout(() => resolve(1), 3000)), // 1
new Promise((resolve, reject) => setTimeout(() => resolve(2), 2000)), // 2
new Promise((resolve, reject) => setTimeout(() => resolve(3), 1000)) // 3
]).then(alert); // 1,2,3 when promises are ready: each promise contributes an array member
```
Please note that the relative order is the same. Even though the first promise takes the longest time to resolve, it is still first in the array of results.
A more real-life example with fetching user information for an array of github users by their names:
A common trick is to map an array of job data into an array of promises, and then wrap that into `Promise.all`.
For instance, if we have an array of URLs, we can fetch them all like this:
```js run
let names = ['iliakan', 'remy', 'jresig'];
let urls = [
'https://api.github.com/users/iliakan',
'https://api.github.com/users/remy',
'https://api.github.com/users/jeresig'
];
// map every url to the promise fetch(github url)
let requests = names.map(name => fetch(`https://api.github.com/users/${name}`));
let requests = urls.map(url => fetch(url));
// Promise.all waits until all jobs are resolved
Promise.all(requests)
.then(responses => responses.forEach(
response => alert(`${response.url}: ${response.status}`)
));
```
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):
```js run
let names = ['iliakan', 'remy', 'jeresig'];
let requests = names.map(name => fetch(`https://api.github.com/users/${name}`));
Promise.all(requests)
.then(responses => {
// all responses are ready, we can show HTTP status codes
@ -103,13 +122,13 @@ Promise.all(requests)
return responses;
})
// map array of responses into array of response.json() and wrap them info Promise.all
// map array of responses into array of response.json() to read their content
.then(responses => Promise.all(responses.map(r => r.json())))
// all JSON answers are parsed: users is the array of them
// all JSON answers are parsed: "users" is the array of them
.then(users => users.forEach(user => alert(user.name)));
```
If any of the promises is rejected, `Promise.all` also rejects with that error.
If any of the promises is rejected, `Promise.all` immediately rejects with that error.
For instance:
@ -124,12 +143,14 @@ Promise.all([
]).catch(alert); // Error: Whoops!
```
Here the `.catch` runs after 2 seconds, when the second promise rejects, and the rejection error becomes the outcome of the whole `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 in the example above all promises finally settle, but in case of an error all results are ignored, "thrown away".
The important detail is that promises provide no way to "cancel" or "abort" their execution. So other promises continue to execute, and the eventually settle, but all their results are ignored.
````smart header="`Promise.all` wraps non-promise arguments into `Promise.resolve`"
Normally, `Promise.all(iterable)` accepts an iterable of promise objects. But if any of those objects is not a promise, it's wrapped in `Promise.resolve`.
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).
````smart header="`Promise.all(iterable)` allows non-promise items in `iterable`"
Normally, `Promise.all(iterable)` 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`.
For instance, here the results are `[1, 2, 3]`:
@ -167,7 +188,7 @@ Promise.race([
]).then(alert); // 1
```
So, the first result/error becomes the result of the whole `Promise.race`, and further results/errors are ignored.
So, the first result/error becomes the result of the whole `Promise.race`. After the first settled promise "wins the race", all further results/errors are ignored.
## Summary
@ -177,3 +198,5 @@ There are 4 static methods of `Promise` class:
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.
Of these four, `Promise.all` is the most common in practice.

View file

@ -0,0 +1,49 @@
There are no tricks here. Just replace `.catch` with `try...catch` inside `demoGithubUser` and add `async/await` where needed:
```js run
class HttpError extends Error {
constructor(response) {
super(`${response.status} for ${response.url}`);
this.name = 'HttpError';
this.response = response;
}
}
async function loadJson(url) {
let response = await fetch(url);
if (response.status == 200) {
return response.json();
} else {
throw new HttpError(response);
}
}
// Ask for a user name until github returns a valid user
async function demoGithubUser() {
let user;
while(true) {
let name = prompt("Enter a name?", "iliakan");
try {
user = await loadJson(`https://api.github.com/users/${name}`);
break; // no error, exit loop
} catch(err) {
if (err instanceof HttpError && err.response.status == 404) {
// loop continues after the alert
alert("No such user, please reenter.");
} else {
// unknown error, rethrow
throw err;
}
}
}
alert(`Full name: ${user.name}.`);
return user;
}
demoGithubUser();
```

View file

@ -0,0 +1,48 @@
# Rewrite "rethrow" async/await
Rewrite the "rethrow" example from the chapter <info:promise-chaining> using `async/await` instead of `.then/catch`.
And get rid of recursion in favour of a loop in `demoGithubUser`: with `async/await` that becomes possible and is easier to develop later on.
```js run
class HttpError extends Error {
constructor(response) {
super(`${response.status} for ${response.url}`);
this.name = 'HttpError';
this.response = response;
}
}
function loadJson(url) {
return fetch(url)
.then(response => {
if (response.status == 200) {
return response.json();
} else {
throw new HttpError(response);
}
})
}
// Ask for a user name until github returns a valid user
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();
```

View file

@ -0,0 +1,33 @@
The notes are below the code:
```js run
async function loadJson(url) { // (1)
let response = await fetch(url); // (2)
if (response.status == 200) {
let json = await response.json(); // (3)
return json;
}
throw new Error(response.status);
}
loadJson('no-such-user.json')
.catch(alert); // Error: 404 (4)
```
Notes:
1. The function `loadUrl` becomes `async`.
2. All `.then` inside are replaced with `await`.
3. We can `return response.json()` instead of awaiting for it, like this:
```js
if (response.status == 200) {
return response.json(); // (3)
}
```
Then the outer code would have to `await` for that promise to resolve. In our case it doesn't matter.
4. The error thrown from `loadJson` is handled by `.catch`. We can't use `await loadJson(…)` there, because we're not in an `async` function.

View file

@ -0,0 +1,20 @@
# Rewrite using async/await
Rewrite the one of examples from the chapter <info:promise-chaining> using `async/await` instead of `.then/catch`:
```js run
function loadJson(url) {
return fetch(url)
.then(response => {
if (response.status == 200) {
return response.json();
} else {
throw new Error(response.status);
}
})
}
loadJson('no-such-user.json') // (3)
.catch(alert); // Error: 404
```

View file

@ -34,9 +34,7 @@ async function f() {
f().then(alert); // 1
```
So, `async` ensures that the function returns a promise.
But not only that. There's another keyword `await` that works only inside `async` functions.
So, `async` ensures that the function returns a promise, wraps non-promises in it. Simple enough, right? But not only that. There's another keyword `await` that works only inside `async` functions, and is pretty cool.
## Await
@ -47,10 +45,9 @@ The syntax:
let value = await promise;
```
The keyword `await` before a promise makes JavaScript to wait until that promise settles and return its result.
For instance, the code below shows "done!" after one second:
The keyword `await` makes JavaScript wait until that promise settles and returns its result.
Here's example with a promise that resolves in 1 second:
```js run
async function f() {
@ -59,7 +56,7 @@ async function f() {
});
*!*
let result = await promise; // wait till the promise resolves
let result = await promise; // wait till the promise resolves (*)
*/!*
alert(result); // "done!"
@ -68,36 +65,45 @@ async function f() {
f();
```
Let's emphasize that: `await` literally makes JavaScript to wait until the promise settles, and then continue with the result. That doesn't cost any CPU resources, because the engine can do other jobs meanwhile: execute other scripts, handle events etc.
The function execution "pauses" at the line `(*)` and resumes when the promise settles, with `result` becoming its result. So the code above shows "done!" in one second.
It's just a more elegant syntax of getting promise result than `promise.then`.
Let's emphasize that: `await` literally makes JavaScript wait until the promise settles, and then go on with the result. That doesn't cost any CPU resources, because the engine can do other jobs meanwhile: execute other scripts, handle events etc.
It's just a more elegant syntax of getting promise result than `promise.then`, easier to read and write.
````warn header="Can't use `await` in regular functions"
If we try to use `await` in non-async function, that would be a syntax error:
```js run
function f() {
let promise = Promise.resolve(1); // any promise
let promise = Promise.resolve(1);
*!*
let result = await promise; // Syntax error
*/!*
}
```
Usually we get such error when we forget to put `async` before a function.
We can get such error when forgot to put `async` before a function. As said, `await` only works inside `async function`.
````
Let's take an avatar-showing example from the chapter <info:promise-chaining> and rewrite it using `async/await`:
Let's take `showAvatar()` example from the chapter <info:promise-chaining> and rewrite it using `async/await`:
1. First we'll need to replace `.then` calls by `await`.
2. And the function should become `async` for them to work.
```js run
async function showAvatar() {
// read our JSON
let response = await fetch('/article/promise-chaining/user.json');
let user = await response.json();
// read github user
let githubResponse = await fetch(`https://api.github.com/users/${user.name}`);
let githubUser = await githubResponse.json();
// show the avatar
let img = document.createElement('img');
img.src = githubUser.avatar_url;
img.className = "promise-avatar-example";
@ -114,20 +120,20 @@ async function showAvatar() {
showAvatar();
```
Pretty clean and easy to read, right? And works the same as before.
Pretty clean and easy to read, right?
Once again, please note that we can't write `await` in top-level code:
Please note that we can't write `await` in top-level code. That wouldn't work:
```js
// syntax error
```js run
// syntax error in top-level code
let response = await fetch('/article/promise-chaining/user.json');
let user = await response.json();
```
...We need to wrap it into an async function.
...We need to wrap "awaiting" commands into an async function instead.
````smart header="Await accepts thenables"
Like `promise.then`, `await` allows to use thenable objects (those with a callable `then` method). Again, the idea is that a 3rd-party object may be promise-compatible: if it supports `.then`, that's enough.
Like `promise.then`, `await` allows to use thenable objects (those with a callable `then` method). Again, the idea is that a 3rd-party object may be promise-compatible: if it supports `.then`, that's enough to use with `await`.
For instance:
```js run
@ -150,18 +156,21 @@ async function f() {
f();
```
Just like with promise chains, if `await` detects an object with `.then`, it calls that method providing native functions `resolve`, `reject` as arguments. Then `await` waits until one of them is called `(*)`and proceeds with the result.
If `await` gets an object with `.then`, it calls that method providing native functions `resolve`, `reject` as arguments. Then `await` waits until one of them is called (in the example above it happens in the line `(*)`) and then proceeds with the result.
````
## Error handling
If a promise resolves normally, then `await promise` returns the result. But in case of a rejection it throws an error, just if there were a `throw` statement at that line.
If a promise resolves normally, then `await promise` returns the result. But in case of a rejection it throws the error, just if there were a `throw` statement at that line.
This code:
```js
async function f() {
*!*
await Promise.reject(new Error("Whoops!"));
*/!*
}
```
@ -169,11 +178,13 @@ async function f() {
```js
async function f() {
*!*
throw new Error("Whoops!");
*/!*
}
```
In real situations the promise may take time before it rejects. So `await` will wait for some time, then throw an error.
In real situations the promise may take some time before it rejects. So `await` will wait, and then throw an error.
We can catch that error using `try..catch`, the same way as a regular `throw`:
@ -192,7 +203,7 @@ async function f() {
f();
```
In case of an error, the control jumps to the `catch`, so we can wrap multiple lines:
In case of an error, the control jumps to the `catch` block. We can also wrap multiple lines:
```js run
async function f() {
@ -209,19 +220,20 @@ async function f() {
f();
```
If we don't have `try..catch`, then the promise generated by the async function `f` becomes rejected, so we can catch the error on it like this:
If we don't have `try..catch`, then the promise generated by the call of the async function `f()` becomes rejected. We can append `.catch` to handle it:
```js run
async function f() {
let response = await fetch('http://no-such-url');
}
// f() becomes a rejected promise
*!*
f().catch(alert); // TypeError: failed to fetch // (*)
*/!*
```
If we also forget to add `.catch` there, then we get an unhandled promise error. We can catch such errors using a global event handler as described in the chapter <info:promise-chaining>.
If we forget to add `.catch` there, then we get an unhandled promise error (and can see it in the console). We can catch such errors using a global event handler as described in the chapter <info:promise-chaining>.
```smart header="`async/await` and `promise.then/catch`"
@ -247,3 +259,19 @@ let results = await Promise.all([
In case of an error, it propagates as usual: from the failed promise to `Promise.all`, and then becomes an exception that we can catch using `try..catch` around the call.
````
## Summary
The `async` keyword before a function has two effects:
1. Makes it always return a promise.
2. Allows to use `await` in it.
The `await` keyword before a promise makes JavaScript wait until that promise settles, and then:
1. If it's an error, the exception is generated, same as if `throw error` were called at that very place.
2. Otherwise, it returns the result, so we can assign it to a value.
Together they provide a great framework to write asynchronous code that is easy both to read and write.
With `async/await` we rarely need to write `promise.then/catch`, but we still shouldn't forget that they are based on promises, because sometimes (e.g. in the outmost scope) we have to use these methods. Also `Promise.all` is a nice thing to wait for many tasks simultaneously.

View file

@ -0,0 +1,8 @@
<style>
.promise-avatar-example {
border-radius: 50%;
position: fixed;
left: 10px;
top: 10px;
}
</style>

View file

@ -1,5 +1,2 @@
development: true
---
# The art of async programming: promises, async/await