This commit is contained in:
Ilya Kantor 2017-05-25 11:56:32 +03:00
parent 787d58a83f
commit 455d300d8d
280 changed files with 2 additions and 2 deletions

View file

@ -0,0 +1,59 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
.message-ball {
font-size: 20px;
line-height: 200px;
text-align: center;
}
.circle {
transition-property: width, height, margin-left, margin-top;
transition-duration: 2s;
position: fixed;
transform: translateX(-50%) translateY(-50%);
background-color: red;
border-radius: 50%;
}
</style>
</head>
<body>
<button onclick="go()">Click me</button>
<script>
function go() {
showCircle(150, 150, 100, div => {
div.classList.add('message-ball');
div.append("Hello, world!");
});
}
function showCircle(cx, cy, radius, callback) {
let div = document.createElement('div');
div.style.width = 0;
div.style.height = 0;
div.style.left = cx + 'px';
div.style.top = cy + 'px';
div.className = 'circle';
document.body.append(div);
setTimeout(() => {
div.style.width = radius * 2 + 'px';
div.style.height = radius * 2 + 'px';
div.addEventListener('transitionend', function handler() {
div.removeEventListener('transitionend', handler);
callback(div);
});
}, 0);
}
</script>
</body>
</html>

View file

@ -0,0 +1,25 @@
# Animated circle with callback
In the task <info:task/animate-circle> an animated growing circle is shown.
Now let's say we need not just a circle, but to show a message inside it. The message should appear *after* the animation is complete (the circle is fully grown), otherwise it would look ugly.
In the solution of the task, the function `showCircle(cx, cy, radius)` draws the circle, but gives no way to track when it's ready.
Add a callback argument: `showCircle(cx, cy, radius, callback)` to be called when the animation is complete. The `callback` should receive the circle `<div>` as an argument.
Here's the example:
```js
showCircle(150, 150, 100, div => {
div.classList.add('message-ball');
div.append("Hello, world!");
});
```
Demo:
[iframe src="solution" height=260]
Take the solution of the task <info:task/animate-circle> as the base.

View file

@ -0,0 +1,266 @@
# Introduction: sync vs async, callbacks
Many actions in Javascript are *asynchronous*.
For instance, take a look at the function `loadScript(src)` that loads a script:
```js
function loadScript(src) {
let script = document.createElement('script');
script.src = src;
document.head.append(script);
}
```
The purpose of the function is to load a new script. When it adds the `<script src="…">` to the document, the browser loads and executes it.
We can use it like this:
```js
// loads and executes the script
loadScript('/my/script.js');
```
The function is called "asynchronous", because the action (script loading) finishes not now, but later.
The call initiates the script loading, then the execution continues normally.
```js
loadScript('/my/script.js');
// the code below doesn't wait for the script loading to finish
```
Now let's say we want to use the new script when loads. It probably declares new functions, so we'd like to run them.
...But if we do that immediately after the `loadScript(…)` call, that wouldn't work:
```js
loadScript('/my/script.js'); // the script has "function newFunction() {…}"
*!*
newFunction(); // no such function!
*/!*
```
Naturally, the browser probably didn't have the time to load the script. As of now, the `loadScript` function doesn't provide a way to track the load completion. The script loads and eventually runs, that's all. But we'd like to know when happens, to use new functions and variables from that script.
Let's add a `callback` function as a second argument to `loadScript` that should execute when the script loads:
```js
function loadScript(src, callback) {
let script = document.createElement('script');
script.src = src;
*!*
script.onload = () => callback(script);
*/!*
document.head.append(script);
}
```
Now if we want to call new functions from the script, we should write that in the callback:
```js
loadScript('/my/script.js', function() {
// the callback runs after the script is loaded
newFunction(); // so now it works
...
});
```
That's the idea: the second argument is a function (usually anonymous) that runs when the action is completed.
Here's a runnable example with the real script:
```js run
function loadScript(src, callback) {
let script = document.createElement('script');
script.src = src;
script.onload = () => callback(script);
document.head.append(script);
}
*!*
loadScript('https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js', script => {
alert(`Cool, the ${script.src} is loaded`);
alert( _ ); // function declared in the loaded script
});
*/!*
```
That's called a "callback-based" style of asynchronous programming. A function that does something asynchronously should provide a `callback` argument where we put the function to run after it's complete.
Here we did so in `loadScript`, but of course it's a general approach.
## Callback in callback
How to load two scripts sequentially: the first one, and then the second one after it?
The natural solution would be to put the second `loadScript` call inside the callback, like this:
```js
loadScript('/my/script.js', function(script) {
alert(`Cool, the ${script.src} is loaded, let's load one more`);
*!*
loadScript('/my/script2.js', function(script) {
alert(`Cool, the second script is loaded`);
});
*/!*
});
```
After the outer `loadScript` is complete, the callback initiates the inner one.
...What if we want one more script?
```js
loadScript('/my/script.js', function(script) {
loadScript('/my/script2.js', function(script) {
*!*
loadScript('/my/script3.js', function(script) {
// ...continue after all scripts are loaded
});
*/!*
})
});
```
## Handling errors
In examples above we didn't consider errors. What if a script loading fails with an error? Our callback should be able to react on that.
Here's an improved version of `loadScript` that tracks loading errors:
```js run
function loadScript(src, callback) {
let script = document.createElement('script');
script.src = src;
*!*
script.onload = () => callback(null, script);
script.onerror = () => callback(new Error(`Script load error ` + src));
*/!*
document.head.append(script);
}
```
It calls `callback(null, script)` for successful load and `callback(error)` otherwise.
The usage:
```js
loadScript('/my/script.js', function(error, script) {
if (error) {
// handle error
} else {
// script loaded successfully
}
});
```
Once again, the recipe that we used for `loadScript` is actually quite common. It's called the "error-first callback" style.
The convention is:
1. The first argument of `callback` is reserved for an error if it occurs. Then `callback(err)` is called.
2. The second argument and successive ones if needed are for the successful result. Then `callback(null, result1, result2…)` is called.
So the single `callback` function is used both for reporting errors and passing back results.
## Pyramid of doom
From the first look it's a viable way of asynchronous coding. And indeed it is. For one or maybe two nested calls it looks fine.
But for multiple asynchronous actions that follow one after another we'll have a code like this:
```js
loadScript('1.js', function(error, script) {
if (error) {
handleError(error);
} else {
// ...
loadScript('2.js', function(error, script) {
if (error) {
handleError(error);
} else {
// ...
loadScript('3.js', function(error, script) {
if (error) {
handleError(error);
} else {
*!*
// ...continue after all scripts are loaded (*)
*/!*
}
});
}
})
}
});
```
In the code above:
1. We load `1.js`, then if there's no error.
2. We load `2.js`, then if there's no error.
3. We load `3.js`, then if there's no error -- do something else `(*)`.
As calls become more nested, the code becomes deeper and increasingly more difficult to manage, especially if we have a real code instead of `...`, that may include more loops, conditional statements and so on.
That's sometimes called "callback hell" or "pyramid of doom".
![](callback-hell.png)
The "pyramid" of nested calls grows to the right with every asynchronous action. Soon it spirales out of control.
So this way of coding appears not so good.
We can try to alleviate the problem by making every action a standalone function, like this:
```js
loadScript('1.js', step1);
function step1(error, script) {
if (error) {
handleError(error);
} else {
// ...
loadScript('2.js', step2);
}
}
function step2(error, script) {
if (error) {
handleError(error);
} else {
// ...
loadScript('3.js', step3);
}
}
function step3(error, script) {
if (error) {
handleError(error);
} else {
// ...continue after all scripts are loaded (*)
}
};
```
See? It does the same, and there's no deep nesting now, because we made every action a separate top-level function. It works, but the code looks like a torn apart spreadsheet. It's difficult to read. One needs to eye-jump between pieces while reading it. That's inconvenient, especially the reader is not familiar with the code and doesn't know where to eye-jump.
Also the functions named `step*` are all of a single use, they are only created to evade the "pyramid of doom". So there's a bit of a namespace cluttering here.
We'd like to have a better way of coding for complex asynchronous actions.
Luckily, there are other ways to evade such pyramids. One of the best ways is to use "promises", described in the next chapter.

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

View file

@ -0,0 +1,3 @@
function one() {
alert(1);
}

View file

@ -0,0 +1,3 @@
The output is: `1`.
The second call to `resolve` is ignored, because only the first call of `reject/resolve` is taken into account. Further calls are ignored.

View file

@ -0,0 +1,15 @@
# Re-resolve a promise?
What's the output of the code below?
```js
let promise = new Promise(function(resolve, reject) {
resolve(1);
setTimeout(() => resolve(2), 1000);
});
promise.then(alert);
```

View file

@ -0,0 +1,9 @@
```js run
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
delay(3000).then(() => alert('runs after 3 seconds'));
```
Please note that in this task `resolve` is called without arguments. We don't return any value from `delay`, just ensure the delay.

View file

@ -0,0 +1,14 @@
# Delay with a promise
The built-in function `setTimeout` uses callbacks. Create a promise-based alternative.
The function `delay(ms)` should return a promise. That promise should resolve after `ms` milliseconds, so that we can add `.then` to it, like this:
```js
function delay(ms) {
// your code
}
delay(3000).then(() => alert('runs after 3 seconds'));
```

View file

@ -0,0 +1,61 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
.message-ball {
font-size: 20px;
line-height: 200px;
text-align: center;
}
.circle {
transition-property: width, height, margin-left, margin-top;
transition-duration: 2s;
position: fixed;
transform: translateX(-50%) translateY(-50%);
background-color: red;
border-radius: 50%;
}
</style>
</head>
<body>
<button onclick="go()">Click me</button>
<script>
function go() {
showCircle(150, 150, 100).then(div => {
div.classList.add('message-ball');
div.append("Hello, world!");
});
}
function showCircle(cx, cy, radius) {
let div = document.createElement('div');
div.style.width = 0;
div.style.height = 0;
div.style.left = cx + 'px';
div.style.top = cy + 'px';
div.className = 'circle';
document.body.append(div);
return new Promise(resolve => {
setTimeout(() => {
div.style.width = radius * 2 + 'px';
div.style.height = radius * 2 + 'px';
div.addEventListener('transitionend', function handler() {
div.removeEventListener('transitionend', handler);
resolve(div);
});
}, 0);
})
}
</script>
</body>
</html>

View file

@ -0,0 +1,15 @@
# Animated circle with promise
Rewrite the `showCircle` function in the solution of the task <info:task/animate-circle-callback> so that it returns a promise instead of accepting a callback.
The new usage:
```js
showCircle(150, 150, 100).then(div => {
div.classList.add('message-ball');
div.append("Hello, world!");
});
```
Take the solution of the task <info:task/animate-circle-callback> as the base.

View file

@ -0,0 +1,283 @@
# Promise
Imagine that you're a top singer, and fans ask for your next upcoming single day and night.
To get a relief, you promise to send it to them when it's published. You give your fans a list. They can fill in their coordinates, so that when the song becomes available, all subscribed parties instantly get it. And if something goes very wrong, so that the song won't be published ever, then they are also to be notified.
Everyone is happy: you, because the people don't crowd you any more, and fans, because they won't miss the single.
That was a real-life analogy for things we often have in programming:
1. A "producing code" that does something and needs time. For instance, it loads a remote script. That's the "singer".
2. A "consuming code" wants the result when it's ready. Many functions may need that result. So that's the "fans".
3. A *promise* is a special JavaScript object that links them together. That's the "list". The producing code makes it and gives to everyone, so that they can subscribe for the result.
The analogy isn't very accurate, because JavaScript promises are more complex than a simple list: they have additional features and limitations. But still they are alike.
The constructor syntax for a promise object is:
```js
let promise = new Promise(function(resolve, reject) {
// producing code
});
```
The function passed to `new Promise` is called *executor*. When the promise is created, it's called automatically. It contains the producing code, that should eventually finish with a result. In terms of the analogy above, the executor is a "singer".
The `promise` object has internal properties:
- `state` -- initially is "pending", then changes to "fulfilled" or "rejected",
- `result` -- an arbitrary value, initially `undefined`.
Both `state` and `result` are managed by the executor.
When the executor finishes the job, it should call one of:
- `resolve(value)` -- to indicate that the job finished successfully:
- sets `state` to `"fulfilled"`,
- sets `result` to `value`.
- `reject(error)` -- to indicate that an error occured:
- sets `state` to `"rejected"`,
- sets `result` to `error`.
![](promise-resolve-reject.png)
Here's a simple executor, just to see it in action:
```js run
let promise = new Promise(function(resolve, reject) {
// the function is executed automatically when the promise is constructed
alert(resolve); // function () { [native code] }
alert(reject); // function () { [native code] }
// after 1 second signal that the job is done with the result "done!"
setTimeout(() => *!*resolve("done!")*/!*, 1000);
});
```
We can see two things by running the code above:
1. The executor is called automatically and immediately (by `new Promise`).
2. Executor arguments `resolve` and `reject` are functions that come from JavaScript engine. We don't need to create them (their string representation may vary between JavaScript engines). Instead the executor should call them when ready.
After one second of thinking it calls `resolve("done")` to produce the result:
![](promise-resolve-1.png)
The next example rejects the promise with an error:
```js
let promise = new Promise(function(resolve, reject) {
// after 1 second signal that the job is finished with an error
setTimeout(() => *!*reject(new Error("Whoops!"))*/!*, 1000);
});
```
![](promise-reject-1.png)
The promise that is either resolved or rejected is called "settled", as opposed to a "pending" promise.
````smart header="There can be only one result or an error"
The executor should call only one `resolve` or `reject`. And the promise state change is final.
All further calls of `resolve` and `reject` are ignored:
```js
let promise = new Promise(function(resolve, reject) {
resolve("done");
reject(new Error("…")); // ignored
setTimeout(() => resolve("…")); // ignored
});
```
The idea is that a job done by the executor may have only one result or an error. In programming, there exist other data structures that allow many "flowing" results, for instance streams and queues. They have their own advantages and disadvantages versus promises. They are not supported by JavaScript core and lack certain language features that promises provide, so we don't cover them here, to concentrate on promises.
Also we if we call `resolve/reject` with more then one argument -- only the first argument is used, the next ones are ignored.
````
```smart header="Reject with `Error` objects"
Technically we can call `reject` (just like `resolve`) with any type of argument. But it's recommended to use `Error` objects in `reject` (or inherit from them). The reasoning for that will become obvious soon.
```
````smart header="Resolve/reject can be immediate"
In practice an executor usually does something asynchronously and calls `resolve/reject` after some time, but it doesn't have to. We can call `resolve` or `reject` immediately, like this:
```js
let promise = new Promise(function(resolve, reject) {
resolve(123); // immediately give the result: 123
});
```
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.
````
```smart header="The `state` and `result` are internal"
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.
```
## Consumers: ".then" and ".catch"
A promise object serves as a link between the producing code (executor) and the consuming code. "Consumers" -- functions that want to receive the result/error can be registered using methods `promise.then` and `promise.catch`.
The syntax of `.then` is:
```js
promise.then(
function(result) { /* handle a sucessful result */ },
function(error) { /* handle an error */ }
);
```
The first function argument runs when the promise is resolved and gets the result, and the second one -- when it's rejected and gets the error.
For instance:
```js run
let promise = new Promise(function(resolve, reject) {
setTimeout(() => resolve("done!"), 1000);
});
// resolve runs the first function in .then
promise.then(
*!*
result => alert(result), // shows "done!" after 1 second
*/!*
error => alert(error) // doesn't run
);
```
In case of a rejection:
```js run
let promise = new Promise(function(resolve, reject) {
setTimeout(() => reject(new Error("Whoops!")), 1000);
});
// reject runs the second function in .then
promise.then(
result => alert(result), // doesn't run
*!*
error => alert(error) // shows "Error: Whoops!" after 1 second
*/!*
);
```
If we're interested only in successful completions, then we can provide only one argument to `.then`:
```js run
let promise = new Promise(resolve => {
setTimeout(() => resolve("done!"), 1000);
});
*!*
promise.then(alert); // shows "done!" after 1 second
*/!*
```
If we're interested only in errors, then we can use `.then(null, function)` or an "alias" to it: `.catch(function)`
```js run
let promise = new Promise((resolve, reject) => {
setTimeout(() => reject(new Error("Whoops!")), 1000);
});
*!*
// .catch(f) is the same as promise.then(null, f)
promise.catch(alert); // shows "Error: Whoops!" after 1 second
*/!*
```
So, `.catch(f)` is a complete analog of `.then(null, f)`, just a shorthand.
````smart header="On settled promises `then` runs immediately"
If a promise is pending, `.then/catch` handlers wait for the result. Otherwise, if a promise has already settled, they execute immediately:
```js run
// an immediately resolved promise
let promise = new Promise(resolve => resolve("done!"));
promise.then(alert); // done! (shows up right now)
```
That's good for jobs that may sometimes require time and sometimes finish immediately. The handler is guaranteed to run in both cases.
To be precise, `.then/catch` queue up and are taken from the queue asynchronously when the current code finishes, like `setTimeout(..., 0)`.
So here the `alert` call is "queued" and runs immediately after the code finishes:
```js run
// an immediately resolved promise
let promise = new Promise(resolve => resolve("done!"));
promise.then(alert); // done! (right after the current code finishes)
alert("code finished"); // this alert shows first
```
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.
````
Now let's see more practical examples to see how promises can help us in writing asynchronous code.
## Example: loadScript
We have the `loadScript` function for loading a script from the previous chapter.
Here's the callback-based variant, just to remind it:
```js
function loadScript(src, callback) {
let script = document.createElement('script');
script.src = src;
script.onload = () => callback(null, script);
script.onerror = () => callback(new Error(`Script load error ` + src));
document.head.append(script);
}
```
Let's rewrite it using promises.
The new function `loadScript` will not require a callback. Instead it will create and return a promise object that settles when the loading is complete. The outer code can add handlers to it using `.then`:
```js run
function loadScript(src) {
return new Promise(function(resolve, reject) {
let script = document.createElement('script');
script.src = src;
script.onload = () => resolve(script);
script.onerror = () => reject(new Error("Script load error: " + src));
document.head.append(script);
});
}
```
Usage:
```js run
let promise = loadScript("https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js");
promise.then(
script => alert(`${script.src} is loaded!`),
error => alert(`Error: ${error.message}`)
);
promise.then(script => alert('One more handler to do something else!'));
```
We can immediately see few benefits over the callback-based syntax:
```compare minus="Callbacks" plus="Promises"
- We must have a ready `callback` function when calling `loadScript`. In other words, we must know what to do with the result *before* `loadScript` is called.
- There can be only one callback.
+ Promises allow us to code more naturally. First we run `loadScript`, and `.then` code what we do with the result.
+ We can call `.then` as many times as we want, at any point of time later.
```
So promises already give us better code flow and flexibility. But there's more. We'll see that in the next chapters.

View file

@ -0,0 +1,13 @@
<script>
function loadScript(src) {
return new Promise(function(resolve, reject) {
let script = document.createElement('script');
script.src = src;
script.onload = () => resolve(script);
script.onerror = () => reject(new Error("Script load error: " + src));
document.head.append(script);
});
}
</script>

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

View file

@ -0,0 +1,20 @@
The short answer is: **no, they are not the equal**:
The difference is that if an error happens in `f1`, then it is handled by `.catch` here:
```js run
promise
.then(f1)
.catch(f2);
```
...But not here:
```js run
promise
.then(f1, f2);
```
That's because an error is passed down the chain, and in the second code piece there's no chain below `f1`.
In other words, `.then` passes results/errors to the next `.then/catch`. So in the first example, there's a `catch` below, and in the second one -- there isn't, so the error is unhandled.

View file

@ -0,0 +1,12 @@
# Promise: then versus catch
Are these code fragments equal? In other words, do they behave the same way in any circumstances, for any handler functions?
```js
promise.then(f1, f2);
```
Versus;
```js
promise.then(f1).catch(f2);
```

View file

@ -0,0 +1,13 @@
The answer is: **no, it won't**:
```js run
new Promise(function(resolve, reject) {
setTimeout(() => {
throw new Error("Whoops!");
}, 1000);
}).catch(alert);
```
As said in the chapter, there's an "implicit `try..catch`" around the function code. So all synchronous errors are handled.
But here the error is generated not while the executor is running, but later. So the promise can't handle it.

View file

@ -0,0 +1,11 @@
# Error in setTimeout
How do you think, does the `.catch` trigger? Explain your answer?
```js
new Promise(function(resolve, reject) {
setTimeout(() => {
throw new Error("Whoops!");
}, 1000);
}).catch(alert);
```

View file

@ -0,0 +1,684 @@
# Promises chaining
Let's return to the problem mentioned in the chapter <info:callbacks>:
- We have a sequence of asynchronous tasks to be done one after another. For instance, loading scripts.
- How to code it well?
Promises provide a couple of recipes to do that.
[cut]
In this chapter we cover promise chaining.
It looks like this:
```js run
new Promise(function(resolve, reject) {
setTimeout(() => resolve(1), 1000); // (*)
}).then(function(result) { // (**)
alert(result); // 1
return result * 2;
}).then(function(result) { // (***)
alert(result); // 2
return result * 2;
}).then(function(result) {
alert(result); // 4
return result * 2;
});
```
The idea is that the result is passed through the chain of `.then` handlers.
Here the flow is:
1. The initial promise resolves in 1 second `(*)`,
2. Then the `.this` handler is called `(**)`.
3. The value that it returns is passed to the next `.this` handler `(***)`
4. ...and so on.
As the result is passed along the chain of handlers, we can see a sequence of `alert` calls: `1` -> `2` -> `4`.
![](promise-then-chain.png)
The whole thing works, because a call to `promise.then` returns a promise, so that we can call next `.then` on it.
When a handler returns a value, it becomes the result of that promise, so the next `.then` is called with it.
To make things clear, here's the start of the chain:
```js run
new Promise(function(resolve, reject) {
setTimeout(() => resolve(1), 1000);
}).then(function(result) {
alert(result); // 1
return result * 2; // (1)
}) // <-- (2)
```
The value returned by `.then` is a promise, so we can add another `.then` at `(2)`. When the value is returned in `(1)`, that promise becomes resolved, so the next handler triggers with it.
Unlike the chaining, technically we can also add many `.then` to a single promise, like this:
```js run
let promise = new Promise(function(resolve, reject) {
setTimeout(() => resolve(1), 1000);
});
promise.then(function(result) {
alert(result); // 1
return result * 2;
});
promise.then(function(result) {
alert(result); // 1
return result * 2;
});
promise.then(function(result) {
alert(result); // 1
return result * 2;
});
```
...But that's a totally different thing. Here's the picture (compare it with the chaining above):
![](promise-then-many.png)
All `.then` on the same promise get the same result -- the result of that promise. So in the code above all `alert` show the same: `1`. There is no result-passing between them.
In practice we rarely need multiple handlers for one promise. Chaining is used much more often.
## Returning promises
Normally, a value returned by a `.then` handler is immediately passed to the next handler. But there's an exception.
If the returned value is a promise, then the further execution is suspended until it settles. And then the result of that promise is given to the next `.then` handler.
For instance:
```js run
new Promise(function(resolve, reject) {
setTimeout(() => resolve(1), 1000);
}).then(function(result) {
alert(result); // 1
*!*
return new Promise((resolve, reject) => { // (*)
setTimeout(() => resolve(result * 2), 1000);
});
*/!*
}).then(function(result) { // (**)
alert(result); // 2
return new Promise((resolve, reject) => {
setTimeout(() => resolve(result * 2), 1000);
});
}).then(function(result) {
alert(result); // 4
});
```
Here the first `.then` shows `1` returns `new Promise(…)` in the line `(*)`. After one second it resolves, and the result is passed on to handler of the second `.then` in the line `(**)`, that shows `2` and so on.
So the output is again 1 -> 2 > 4, but now with 1 second delay between `alert` calls.
Returning promises allows us to build chains of asynchronous actions.
## Example: loadScript
Let's use this feature with `loadScript` to load scripts one by one, in sequence:
```js run
loadScript("/article/promise-chaining/one.js")
.then(function(script) {
return loadScript("/article/promise-chaining/two.js");
})
.then(function(script) {
return loadScript("/article/promise-chaining/three.js");
})
.then(function(script) {
// use functions declared in scripts
// to show that they indeed loaded
one();
two();
three();
});
```
Here each `loadScript` call returns a promise, and the next `.then` runs when it resolves. Then it initiates the loading of the next script. So scripts are loaded one after another.
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 it is also possible to write `.then` directly after each promise, without returning them, like this:
```js run
loadScript("/article/promise-chaining/one.js").then(function(script1) {
loadScript("/article/promise-chaining/two.js").then(function(script2) {
loadScript("/article/promise-chaining/three.js").then(function(script3) {
// this function has access to variables script1, script2 and script3 (*)
one();
two();
three();
});
});
});
```
This code does the same: loads 3 scripts in sequence. But it "grows to the right". So we have the same problem as with callbacks. Use chaining (return promises from `.then`) to evade it.
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.
````smart header="Thenables"
To be precise, `.then` may return an arbitrary "thenable" object, and it will be treated the same way as a promise.
A "thenable" object is any object with a method `.then`.
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`.
Here's an example of a thenable object:
```js run
class Thenable {
constructor(num) {
this.num = num;
}
then(resolve, reject) {
alert(resolve); // function() { native code }
// resolve with this.num*2 after the 1 secound
setTimeout(() => resolve(this.num * 2), 1000); // (**)
}
}
new Promise(resolve => resolve(1))
.then(result => {
return new Thenable(result); // (*)
})
.then(alert); // shows 2 after 1000ms
```
JavaScript checks the object returned by `.then` handler in the line `(*)`: if it has a callable method named `then`, then it calls that method providing native functions `resolve`, `reject` as arguments (similar to executor) and waits until one of them is called. In the example above `resolve(2)` is called after 1 second `(**)`. Then the result is passed further down the chain.
This feature allows to integrate custom objects with promise chains without having to inherit from `Promise`.
````
## Bigger example: fetch
In frontend programming promises are often used for network requests. So let's see an extended example of that.
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:
```js
let promise = fetch(url);
```
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.
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.
The code below makes a request to `user.json` and loads its text from the server:
```js run
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
return response.text();
})
.then(function(text) {
// ...and here's the content of the remote file
alert(text); // {"name": "iliakan", isAdmin: true}
});
```
There is also a method `response.json()` that reads the remote data and parses it as JSON. In our case that's even more convenient.
We'll also use arrow functions for brevity:
```js run
// 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
```
Now let's do something with the loaded user.
For instance, we can make one more request to github, load the user profile and show the avatar:
```js run
// 1. Make a request for user.json
fetch('/article/promise-chaining/user.json')
// Load it as json
.then(response => response.json())
// Make a request to github
.then(user => fetch(`https://api.github.com/users/${user.name}`))
// Load the response as json
.then(response => response.json())
// Show the avatar image (githubUser.avatar_url) for 3 seconds (maybe animate it)
.then(githubUser => {
let img = document.createElement('img');
img.src = githubUser.avatar_url;
img.className = "promise-avatar-example";
document.body.append(img);
setTimeout(() => img.remove(), 3000); // (*)
});
```
The code works. But there's a potential problem in it, a typical error of those who begin to use promises.
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.
To make the chain extendable, we need to return a promise that resolves when the avatar finishes showing.
Here's how:
```js run
fetch('/article/promise-chaining/user.json')
.then(response => response.json())
.then(user => fetch(`https://api.github.com/users/${user.name}`))
.then(response => response.json())
*!*
.then(githubUser => new Promise(function(resolve, reject) {
*/!*
let img = document.createElement('img');
img.src = githubUser.avatar_url;
img.className = "promise-avatar-example";
document.body.append(img);
setTimeout(() => {
img.remove();
*!*
resolve(githubUser);
*/!*
}, 3000);
}))
// triggers after 3 seconds
.then(githubUser => alert(`Finished showing ${githubUser.name}`));
```
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.
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.
Finally, we can split the code into reusable functions:
```js run
function loadJson(url) {
return fetch(url)
.then(response => response.json());
}
function loadGithubUser(name) {
return fetch(`https://api.github.com/users/${name}`)
.then(response => response.json());
}
function showAvatar(githubUser) {
return new Promise(function(resolve, reject) {
let img = document.createElement('img');
img.src = githubUser.avatar_url;
img.className = "promise-avatar-example";
document.body.append(img);
setTimeout(() => {
img.remove();
resolve(githubUser);
}, 3000);
});
}
// Use them:
loadJson('/article/promise-chaining/user.json')
.then(user => loadGithubUser(user.name))
.then(showAvatar)
.then(githubUser => alert(`Finished showing ${githubUser.name}`));
// ...
```
## Error handling
Asynchronous actions may sometimes fail: in case of an error the corresponding promises becomes rejected. For instance, `fetch` fails if the remote server is not available. We can use `.catch` to handle errors (rejections).
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 server) and `.catch` handles the error:
```js run
*!*
fetch('https://no-such-server.blabla') // rejects
*/!*
.then(response => response.json())
.catch(err => alert(err)) // TypeError: failed to fetch (the text may vary)
```
Or, maybe, everything is all right with the server, but the response is not a valid JSON:
```js run
fetch('/') // fetch works fine now, the server responds successfully
*!*
.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
```
In the example below we append `.catch` to handle all errors in the avatar-loading-and-showing chain:
```js run
fetch('/article/promise-chaining/user.json')
.then(response => response.json())
.then(user => fetch(`https://api.github.com/users/${user.name}`))
.then(response => response.json())
.then(githubUser => new Promise(function(resolve, reject) {
let img = document.createElement('img');
img.src = githubUser.avatar_url;
img.className = "promise-avatar-example";
document.body.append(img);
setTimeout(() => {
img.remove();
resolve(githubUser);
}, 3000);
}))
.catch(error => alert(error.message));
```
Here `.catch` doesn't trigger at all, because there are no errors. But if any of the promises above rejects, then it would execute.
## Implicit try..catch
The code of the executor and promise handlers has an "invisible `try..catch`" around it. If an error happens, it gets caught and treated as a rejection.
For instance, this code:
```js run
new Promise(function(resolve, reject) {
*!*
throw new Error("Whoops!");
*/!*
}).catch(alert); // Error: Whoops!
```
...Works the same way as this:
```js run
new Promise(function(resolve, reject) {
*!*
reject(new Error("Whoops!"));
*/!*
}).catch(alert); // Error: Whoops!
```
The "invisible `try..catch`" around the executor automatically catches the error and treats it as a rejection.
That's so not only in the executor, but in handlers as well. If we `throw` inside `.then` handler, that means a rejected promise, so the control jumps to the nearest error handler.
Here's an example:
```js run
new Promise(function(resolve, reject) {
resolve("ok");
}).then(function(result) {
*!*
throw new Error("Whoops!"); // rejects the promise
*/!*
}).catch(alert); // Error: Whoops!
```
That's so not only for `throw`, but for any errors, including programming errors as well:
```js run
new Promise(function(resolve, reject) {
resolve("ok");
}).then(function(result) {
*!*
blabla(); // no such function
*/!*
}).catch(alert); // ReferenceError: blabla is not defined
```
As a side effect, the final `.catch` not only catches explicit rejections, but also occasional errors in the handlers above.
## Rethrowing
As we already noticed, `.catch` behaves like `try..catch`. We may have as many `.then` 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. If we `throw` inside `.catch`, then the control goes to the next closest error handler. And if we handle the error and finish normally, then it continues to the closest successful `.then` handler.
In the example below the `.catch` successfully handles the error:
```js run
// the execution: catch -> then
new Promise(function(resolve, reject) {
throw new Error("Whoops!");
}).catch(function(error) {
alert("The error is handled, continue normally");
}).then(() => alert("Next successful handler runs"));
```
Here the `.catch` block finishes normally. So the next successful handler is called. Or it could return something, that would be the same.
...And here the `.catch` block analyzes the error and throws it again:
```js run
// the execution: catch -> catch -> then
new Promise(function(resolve, reject) {
throw new Error("Whoops!");
}).catch(function(error) { // (*)
if (error instanceof URIError) {
// handle it
} else {
alert("Can't handle such error");
*!*
throw error; // throwing this or another error jumps to the next catch
*/!*
}
}).then(function() {
/* never runs here */
}).catch(error => { // (**)
alert(`The unknown error has occured: ${error}`);
// don't return anything => execution goes the normal way
});
```
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 `(**)`.
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 the `response` object and saves it in the error. So error-handling code will be able to access it.
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 better message.
The great thing about having our own class for errors is that we can easily check for it in error-handling code.
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}.`); // (1)
return user;
})
.catch(err => {
*!*
if (err instanceof HttpError && err.response.status == 404) { // (2)
*/!*
alert("No such user, please reenter.");
return demoGithubUser();
} else {
throw err;
}
});
}
demoGithubUser();
```
Here:
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.
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.
## Unhandled rejections
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:
```js untrusted run refresh
new Promise(function() {
noSuchFunction(); // Error here (no such function)
});
```
Or here:
```js untrusted run refresh
new Promise(function() {
throw new Error("Whoops!");
}).then(function() {
// ...something...
}).then(function() {
// ...something else...
}).then(function() {
// ...but no catch after it!
});
```
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".
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. We can see it in the console.
In the browser we can catch it using the event `unhandledrejection`:
```js run
*!*
window.addEventListener('unhandledrejection', function(event) {
// the event object has two special properties:
alert(event.promise); // [object Promise] - the promise that generated the error
alert(event.reason); // Error: Whoops! - the unhandled error object
});
*/!*
new Promise(function() {
throw new Error("Whoops!");
}); // no catch to handle the error
```
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.
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.
In non-browser environments like Node.JS there are other similar ways to track unhandled errors.
## Summary
To summarize, `.then/catch(handler)` returns a new promise that changes depending on what handler does:
1. If it returns a value or finishes without a `return` (same as `return undefined`), then the new promise becomes resolved, and the closest resolve handler (the first argument of `.then`) is called with that value.
2. If it throws an error, then the new promise becomes rejected, and the closest rejection handler (second argument of `.then` or `.catch`) is called with it.
3. If it returns a promise, then JavaScript waits until it settles and then acts on its outcome the same way.
The picture of how the promise returned by `.then/catch` changes:
![](promise-handler-variants.png)
The smaller picture of how handlers are called:
![](promise-handler-variants-2.png)
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`.
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.
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.

View file

@ -0,0 +1,3 @@
function getMessage() {
return "Hello, world!";
}

View file

@ -0,0 +1,41 @@
<script>
function loadScript(src) {
return new Promise(function(resolve, reject) {
let script = document.createElement('script');
script.src = src;
script.onload = () => resolve(script);
script.onerror = () => reject(new Error("Script load error: " + src));
document.head.append(script);
});
}
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);
}
})
}
</script>
<style>
.promise-avatar-example {
border-radius: 50%;
position: fixed;
left: 10px;
top: 10px;
}
</style>

View file

@ -0,0 +1,3 @@
function one() {
alert(1);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View file

@ -0,0 +1,3 @@
function three() {
alert(3);
}

View file

@ -0,0 +1,3 @@
function two() {
alert(2);
}

View file

@ -0,0 +1,4 @@
{
"name": "iliakan",
"isAdmin": true
}

View file

@ -0,0 +1,44 @@
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 it detects an error, then it rejects with it. So we need to prevent any error from occuring. 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))
)
```

View file

@ -0,0 +1,24 @@
<!DOCTYPE HTML>
<html>
<body>
<script>
let urls = [
'https://api.github.com/users/iliakan',
'https://api.github.com/users/remy',
'http://no-such-url'
];
Promise.all(
urls.map(url => fetch(url).catch(err => err))
)
.then(responses => {
alert(responses[0].status); // 200
alert(responses[1].status); // 200
alert(responses[2]); // TypeError: failed to fetch (text may vary)
});
</script>
</body>
</html>

View file

@ -0,0 +1,28 @@
<!DOCTYPE HTML>
<html>
<body>
<script>
// if any of URLs fails, other results are ignored
// change that:
// make errors appear as members of the responses array, together with normal results
let urls = [
'https://api.github.com/users/iliakan',
'https://api.github.com/users/remy',
'http://no-such-url'
];
// Fix me:
Promise.all(urls.map(url => fetch(url)))
// Demo output (no need to change):
.then(responses => {
alert(responses[0].status); // 200
alert(responses[1].status); // 200
alert(responses[2]); // TypeError: failed to fetch (text may vary)
));
</script>
</body>
</html>

View file

@ -0,0 +1,48 @@
# 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 loose 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 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.

View file

@ -0,0 +1,29 @@
<!DOCTYPE HTML>
<html>
<body>
<script>
let urls = [
'https://api.github.com/users/iliakan',
'/',
'http://no-such-url'
];
Promise.all(
urls.map(url => fetch(url).catch(err => err))
)
.then(responses => Promise.all(
// if it's an error then pass on
// otherwise response.json() and catch errors as results
responses.map(r => r instanceof Error ? r : r.json().catch(err => err))
))
.then(results => {
alert(results[0].name); // Ilya Kantor
alert(results[1]); // SyntaxError: Unexpected token < in JSON at position 0
alert(results[2]); // TypeError: failed to fetch (text may vary)
});
</script>
</body>
</html>

View file

@ -0,0 +1,33 @@
<!DOCTYPE HTML>
<html>
<body>
<script>
// the whole promise chain fails with an error here
// change that:
// make errors appear as members of the results array
let urls = [
'https://api.github.com/users/iliakan',
// this URL is HTML page, it's invalid JSON, so response.json() fails
'/',
// this URL is invalid, so fetch fails
'http://no-such-url'
];
// Fix it:
Promise.all(urls.map(url => fetch(url)))
.then(responses => Promise.all(
responses.map(r => r.json())
))
// Demo output (no need to change):
.then(results => {
alert(results[0].name); // Ilya Kantor
alert(results[1]); // SyntaxError: Unexpected token < in JSON at position 0
alert(results[2]); // TypeError: failed to fetch (text may vary)
});
</script>
</body>
</html>

View file

@ -0,0 +1,34 @@
# Fault-tolerant fetch with JSON
Improve the solution of the previous task <info:task/promise-errors-as-results>. Now we need not just to call `fetch`, but to load the JSON objects from 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 loose 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.

View file

@ -0,0 +1,202 @@
# Promise API
There are 4 static methods in the `Promise` class. We'll quickly cover their use cases here.
## Promise.resolve
The syntax:
```js
let promise = Promise.resolve(value);
```
Returns a resolved promise with the given `value`.
Same as:
```js
let promise = new Promise(resolve => resolve(value));
```
The method is used when we already have a value, but would like to have it "wrapped" into a promise.
For instance, the `loadCached` function below fetches the `url` and remembers the result, so that future calls on the same URL return it immediately:
```js
function loadCached(url) {
let cache = loadCached.cache || (loadCached.cache = new Map());
if (cache.has(url)) {
*!*
return Promise.resolve(cache.get(url)); // (*)
*/!*
}
return fetch(url)
.then(response => response.text())
.then(text => {
cache[url] = text;
return text;
});
}
```
We can use `loadCached(url).then(…)`, because the function is guaranteed to return a promise. That's the purpose `Promise.resolve` in the line `(*)`: it makes sure the interface unified. We can always use `.then` after `loadCached`.
## Promise.reject
The syntax:
```js
let promise = Promise.reject(error);
```
Create a rejected promise with the `error`.
Same as:
```js
let promise = new Promise((resolve, reject) => reject(error));
```
We cover it here for completeness, rarely used in real code.
## Promise.all
The method to run many promises in parallel and wait till all of them are ready.
The syntax is:
```js
let promise = Promise.all(iterable);
```
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 then its result is an array `[1, 2, 3]`:
```js run
Promise.all([
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 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 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 = 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
for(let response of responses) {
alert(`${response.url}: ${response.status}`); // shows 200 for every url
}
return responses;
})
// 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
.then(users => users.forEach(user => alert(user.name)));
```
If any of the promises is rejected, `Promise.all` immediately rejects with that error.
For instance:
```js run
Promise.all([
new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)),
*!*
new Promise((resolve, reject) => setTimeout(() => reject(new Error("Whoops!")), 2000)),
*/!*
new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000))
]).catch(alert); // Error: Whoops!
```
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 the eventually settle, but all 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).
````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]`:
```js run
Promise.all([
new Promise((resolve, reject) => {
setTimeout(() => resolve(1), 1000)
}),
2, // treated as Promise.resolve(2)
3 // treated as Promise.resolve(3)
]).then(alert); // 1, 2, 3
```
So we are able to pass non-promise values to `Promise.all` where convenient.
````
## Promise.race
Similar to `Promise.all` takes an iterable of promises, but instead of waiting for all of them to finish -- waits for the first result (or error), and goes on with it.
The syntax is:
```js
let promise = Promise.race(iterable);
```
For instance, here the result will be `1`:
```js run
Promise.race([
new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)),
new Promise((resolve, reject) => setTimeout(() => reject(new Error("Whoops!")), 2000)),
new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000))
]).then(alert); // 1
```
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
There are 4 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.
Of these four, `Promise.all` is the most common in practice.

View file

@ -0,0 +1,13 @@
<script>
function loadScript(src) {
return new Promise(function(resolve, reject) {
let script = document.createElement('script');
script.src = src;
script.onload = () => resolve(script);
script.onerror = () => reject(new Error("Script load error: " + src));
document.head.append(script);
});
}
</script>

View file

@ -0,0 +1,4 @@
{
"name": "iliakan",
"isAdmin": true
}

View file

@ -0,0 +1,3 @@
function one() {
alert(1);
}

View file

@ -0,0 +1,3 @@
function two() {
alert(2);
}

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

@ -0,0 +1,277 @@
# Async/await
There's a special syntax to work with promises in a more comfort fashion, called "async/await". It's surprisingly easy to understand and use.
## Async functions
Let's start with the `async` keyword. It can be placed before function, like this:
```js
async function f() {
return 1;
}
```
The word "async" before a function means one simple thing: a function always returns a promise. If it's not so, then the value is wrapped in `Promise.resolve`.
For instance, the code above returns `Promise.resolve(1)`:
```js run
async function f() {
return 1;
}
f().then(alert); // 1
```
...We can explicitly return a promise, that would be the same:
```js run
async function f() {
return Promise.resolve(1);
}
f().then(alert); // 1
```
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
The syntax:
```js
// works only inside async functions
let value = await promise;
```
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() {
let promise = new Promise((resolve, reject) => {
setTimeout(() => resolve("done!"), 1000)
});
*!*
let result = await promise; // wait till the promise resolves (*)
*/!*
alert(result); // "done!"
}
f();
```
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.
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);
*!*
let result = await promise; // Syntax error
*/!*
}
```
We can get such error when forgot to put `async` before a function. As said, `await` only works inside `async function`.
````
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";
document.body.append(img);
// wait 3 seconds
await new Promise((resolve, reject) => setTimeout(resolve, 3000));
img.remove();
return githubUser;
}
showAvatar();
```
Pretty clean and easy to read, right?
Please note that we can't write `await` in top-level code. That wouldn't work:
```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 "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 to use with `await`.
For instance:
```js run
class Thenable {
constructor(num) {
this.num = num;
}
then(resolve, reject) {
alert(resolve); // function() { native code }
// resolve with this.num*2 after 1000ms
setTimeout(() => resolve(this.num * 2), 1000); // (*)
}
};
async function f() {
// waits for 1 second, then result becomes 2
let result = await new Thenable(1);
alert(result);
}
f();
```
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 the error, just if there were a `throw` statement at that line.
This code:
```js
async function f() {
*!*
await Promise.reject(new Error("Whoops!"));
*/!*
}
```
...Is the same as this:
```js
async function f() {
*!*
throw new Error("Whoops!");
*/!*
}
```
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`:
```js run
async function f() {
try {
let response = await fetch('http://no-such-url');
} catch(err) {
*!*
alert(err); // TypeError: failed to fetch
*/!*
}
}
f();
```
In case of an error, the control jumps to the `catch` block. We can also wrap multiple lines:
```js run
async function f() {
try {
let response = await fetch('/no-user-here');
let user = await response.json();
} catch(err) {
// catches errors both in fetch and response.json
alert(err);
}
}
f();
```
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 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`"
When we use `async/await`, we rarely need `.then`, because `await` handles the waiting for us. And we can use a regular `try..catch` instead of `.catch`, that's usually (not always) more convenient.
But at the top-level of the code, when we're calling the outmost `async` function, we're syntactically unable to use `await` (as we're not in an `async` function yet), so it's a normal practice to add `.then/catch` to handle the final result or falling-through errors.
Like in the line `(*)` of the example above.
```
````smart header="Async/await works well with `Promise.all`"
When we need to wait for multiple promises, we can wrap them in `Promise.all` and then `await`:
```js
// wait for the array of results
let results = await Promise.all([
fetch(url1),
fetch(url2),
...
]);
```
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>

2
6-async/index.md Normal file
View file

@ -0,0 +1,2 @@
# Promises, async/await