up
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 4.4 KiB |
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 8.1 KiB |
|
@ -1,8 +1,10 @@
|
||||||
|
|
||||||
# Callback hell
|
|
||||||
|
|
||||||
|
|
||||||
Consider this function `loadScript(src)` that loads a script:
|
# 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
|
```js
|
||||||
function loadScript(src) {
|
function loadScript(src) {
|
||||||
|
@ -12,7 +14,7 @@ function loadScript(src) {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
When the script element is added to the document, the browser loads it and executes. So, the function works.
|
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:
|
We can use it like this:
|
||||||
|
|
||||||
|
@ -21,27 +23,28 @@ We can use it like this:
|
||||||
loadScript('/my/script.js');
|
loadScript('/my/script.js');
|
||||||
```
|
```
|
||||||
|
|
||||||
The function is *asynchronous*: the script starts loading now, but finishes later.
|
The function is called "asynchronous", because the action (script loading) finishes not now, but later.
|
||||||
|
|
||||||
"Synchonous" and "asynchronous" are general programming terms, not specific to JavaScript.
|
The call initiates the script loading, then the execution continues normally.
|
||||||
|
|
||||||
A *synchronous* action suspends the execution until it's completed. For instance, a call to `alert` or `prompt` is synchronous: the program may not continue until it's finished.
|
|
||||||
|
|
||||||
```js
|
|
||||||
let age = prompt("How old are you", 20);
|
|
||||||
// the execution of the code below awaits for the prompt to finish
|
|
||||||
// the script hangs
|
|
||||||
```
|
|
||||||
|
|
||||||
An *asynchronous* action allows the program to continue while it's in progress. For instance, a call to `loadScript` is asynchronous. It initiates the script loading, but does not suspend the execution. Other commands may execute while the script is loading:
|
|
||||||
|
|
||||||
```js
|
```js
|
||||||
loadScript('/my/script.js');
|
loadScript('/my/script.js');
|
||||||
// the execution of the code below *does not* wait for the script loading to finish,
|
// the code below doesn't wait for the script loading to finish
|
||||||
// it just continues
|
|
||||||
```
|
```
|
||||||
|
|
||||||
As of now, the `loadScript` function loads the script, 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 additional functions and variables from that script.
|
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:
|
Let's add a `callback` function as a second argument to `loadScript` that should execute when the script loads:
|
||||||
|
|
||||||
|
@ -58,7 +61,19 @@ function loadScript(src, callback) {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Now we're able to load a script and then run our code that can use new functions from it, like here:
|
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
|
```js run
|
||||||
function loadScript(src, callback) {
|
function loadScript(src, callback) {
|
||||||
|
@ -69,7 +84,7 @@ function loadScript(src, callback) {
|
||||||
}
|
}
|
||||||
|
|
||||||
*!*
|
*!*
|
||||||
loadScript('https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js', function(script) {
|
loadScript('https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js', script => {
|
||||||
alert(`Cool, the ${script.src} is loaded`);
|
alert(`Cool, the ${script.src} is loaded`);
|
||||||
alert( _ ); // function declared in the loaded script
|
alert( _ ); // function declared in the loaded script
|
||||||
});
|
});
|
||||||
|
@ -78,13 +93,13 @@ loadScript('https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js', f
|
||||||
|
|
||||||
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.
|
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 it was used for `loadScript`, but of course it's a general approach.
|
Here we did so in `loadScript`, but of course it's a general approach.
|
||||||
|
|
||||||
## Callback in callback
|
## Callback in callback
|
||||||
|
|
||||||
What if we need to load two scripts sequentially: the first one, and then the second one after it?
|
How to load two scripts sequentially: the first one, and then the second one after it?
|
||||||
|
|
||||||
We can put the second `loadScript` inside the callback, like this:
|
The natural solution would be to put the second `loadScript` call inside the callback, like this:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
loadScript('/my/script.js', function(script) {
|
loadScript('/my/script.js', function(script) {
|
||||||
|
@ -100,7 +115,7 @@ loadScript('/my/script.js', function(script) {
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
Now after the outer `loadScript` is complete, the callback initiates the inner one.
|
After the outer `loadScript` is complete, the callback initiates the inner one.
|
||||||
|
|
||||||
...What if we want one more script?
|
...What if we want one more script?
|
||||||
|
|
||||||
|
@ -120,11 +135,9 @@ loadScript('/my/script.js', function(script) {
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
As we can see, a new asynchronous action means one more nesting level. So the code becomes deeper and deeper.
|
|
||||||
|
|
||||||
## Handling errors
|
## Handling errors
|
||||||
|
|
||||||
In examples above we didn't consider errors. What if a script loading failed with an error? Our callback should be able to react on that.
|
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:
|
Here's an improved version of `loadScript` that tracks loading errors:
|
||||||
|
|
||||||
|
@ -144,7 +157,7 @@ function loadScript(src, callback) {
|
||||||
|
|
||||||
It calls `callback(null, script)` for successful load and `callback(error)` otherwise.
|
It calls `callback(null, script)` for successful load and `callback(error)` otherwise.
|
||||||
|
|
||||||
Usage:
|
The usage:
|
||||||
```js
|
```js
|
||||||
loadScript('/my/script.js', function(error, script) {
|
loadScript('/my/script.js', function(error, script) {
|
||||||
if (error) {
|
if (error) {
|
||||||
|
@ -155,11 +168,13 @@ loadScript('/my/script.js', function(error, script) {
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Once again, the recipe that we used for `loadScript` is actually quite common. It's called the "error-first callback" style.
|
||||||
|
|
||||||
The convention is:
|
The convention is:
|
||||||
1. The first argument of `callback` is reserved for an error if it occurs. Then `callback(err)` is called.
|
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.
|
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. That's called "error-first callback" style. Or we could use different functions for successful and erroneous completion.
|
So the single `callback` function is used both for reporting errors and passing back results.
|
||||||
|
|
||||||
## Pyramid of doom
|
## Pyramid of doom
|
||||||
|
|
||||||
|
@ -200,17 +215,17 @@ In the code above:
|
||||||
2. We load `2.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 `(*)`.
|
3. We load `3.js`, then if there's no error -- do something else `(*)`.
|
||||||
|
|
||||||
As calls become more nested, the whole thing becomes increasingly more difficult to manage, especially if we add real code instead of `...`, that may include more loops, conditional statements and other usage of loaded scripts.
|
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".
|
That's sometimes called "callback hell" or "pyramid of doom".
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
The pyramid grows to the right with every asynchronous action. Soon it spirales out of control.
|
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.
|
So this way of coding appears not so good.
|
||||||
|
|
||||||
In simple cases we can evade the problem by making every action a standalone function, like this:
|
We can try to alleviate the problem by making every action a standalone function, like this:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
loadScript('1.js', step1);
|
loadScript('1.js', step1);
|
||||||
|
@ -242,10 +257,10 @@ function step3(error, script) {
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
See? It does the same, and there's no deep nesting now, because we moved every function to the top. But the code looks like a torn apart spreadsheet. We need to eye-jump between pieces while reading it. It's not very readable, especially if you are not familiar with it and don't know where to eye-jump.
|
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 `step*` have no use, they are only created to evade the "pyramid of doom".
|
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.
|
||||||
|
|
||||||
So we'd like to have a better way of coding for complex asynchronous actions.
|
We'd like to have a better way of coding for complex asynchronous actions.
|
||||||
|
|
||||||
Luckily, there are other ways to evade such pyramids. For instance, we can use "promises", described in the next chapter.
|
Luckily, there are other ways to evade such pyramids. One of the best ways is to use "promises", described in the next chapter.
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 82 KiB |
|
@ -2,19 +2,19 @@
|
||||||
|
|
||||||
Imagine that you're a top singer, and fans ask for your next upcoming single day and night.
|
Imagine that you're a top singer, and fans ask for your next upcoming single day and night.
|
||||||
|
|
||||||
You make a 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 they also can leave a note that if something goes very wrong, so that the song won't be published ever, then they are to be notified.
|
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 people don't crowd you any more, and fans, because they won't miss the single.
|
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:
|
That was a real-life analogy for things we often have in programming:
|
||||||
|
|
||||||
1. A "producing code" does something that needs time. For instance, it loads a remote script. That's the "singer".
|
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".
|
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 singer makes it and gives to fans, so that they can subscribe.
|
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 is not precise: promises are more complex than a simple list. They have additional features and limitations. But still they are alike.
|
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 is:
|
The constructor syntax for a promise object is:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
let promise = new Promise(function(resolve, reject) {
|
let promise = new Promise(function(resolve, reject) {
|
||||||
|
@ -22,23 +22,26 @@ let promise = new Promise(function(resolve, reject) {
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
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. The executor is a "singer".
|
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:
|
The `promise` object has internal properties:
|
||||||
|
|
||||||
- `state` -- initially is "pending", then changes to "fulfilled" or "rejected",
|
- `state` -- initially is "pending", then changes to "fulfilled" or "rejected",
|
||||||
- `result` -- an arbitrary value, initially `undefined`.
|
- `result` -- an arbitrary value, initially `undefined`.
|
||||||
|
|
||||||
When the executor finishes the job, it should change the state of the promise by calling one of:
|
Both `state` and `result` are managed by the executor.
|
||||||
|
|
||||||
- `resolve(value)` -- to indicate that the job finished successfully: sets `state` to `"fulfilled"`, and `result` to `value`.
|
When the executor finishes the job, it should call one of:
|
||||||
- `reject(error)` -- to indicate that an error occured: sets `state` to `"rejected"` and `result` to `error`.
|
|
||||||
|
- `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`.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
The `promise` object can be used to subscribe to the result (like a "list"), soon we'll cover that, but first let's see few code examples of `new Promise`.
|
|
||||||
|
|
||||||
Here's a simple executor, just to see it in action:
|
Here's a simple executor, just to see it in action:
|
||||||
|
|
||||||
```js run
|
```js run
|
||||||
|
@ -47,6 +50,9 @@ let promise = new Promise(function(resolve, reject) {
|
||||||
|
|
||||||
alert(resolve); // function () { [native code] }
|
alert(resolve); // function () { [native code] }
|
||||||
alert(reject); // 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);
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -55,14 +61,7 @@ We can see two things by running the code above:
|
||||||
1. The executor is called automatically and immediately (by `new Promise`).
|
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.
|
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.
|
||||||
|
|
||||||
Now let's make the executor that thinks for 1 second and then produces a result:
|
After one second of thinking it calls `resolve("done")` to produce the result:
|
||||||
|
|
||||||
```js
|
|
||||||
let promise = new Promise(function(resolve, reject) {
|
|
||||||
// after 1 second signal that the job is done with the result "done!"
|
|
||||||
setTimeout(() => *!*resolve("done!")*/!*, 1000);
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
@ -77,10 +76,10 @@ let promise = new Promise(function(resolve, reject) {
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
The promise that was either resolved or rejected is called "settled".
|
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"
|
````smart header="There can be only one result or an error"
|
||||||
We must call only one `resolve` or `reject`. And that's final.
|
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:
|
All further calls of `resolve` and `reject` are ignored:
|
||||||
|
|
||||||
|
@ -93,34 +92,37 @@ let promise = new Promise(function(resolve, reject) {
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
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 results, for instance streams and queues, but we don't cover them here, because they are not supported by JavaScript core, and they lack certain features that promises provide.
|
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 shouldn't call `resolve/reject` with more then one argument: those after the first one are ignored. If we want a complex result, we can return an object.
|
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"
|
```smart header="Reject with `Error` objects"
|
||||||
Technically we can call `resolve` and `reject` with any value. But it's recommended to use `Error` objects in `reject` (or inherited from them). The reasoning for that will become obvious soon.
|
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"
|
````smart header="Resolve/reject can be immediate"
|
||||||
In practice an executor usually does something asynchronously, but it doesn't have to. We can call `resolve` or `reject` immediately, like this:
|
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
|
```js
|
||||||
let promise = new Promise(function(resolve, reject) {
|
let promise = new Promise(function(resolve, reject) {
|
||||||
resolve(123);
|
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 before. Technically that's fine: we have a resolved promise right now.
|
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="Promise properties are internal"
|
||||||
|
Properties of promise objects like `state` and `result` are internal. We can't directly access them from our code.
|
||||||
|
```
|
||||||
|
|
||||||
## Consumers: ".then" and ".catch"
|
## Consumers: ".then" and ".catch"
|
||||||
|
|
||||||
A promise object allows to add "consumers" -- handler functions to run when it resolves/rejects.
|
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 is:
|
The syntax of `.then` is:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
promise.then(
|
promise.then(
|
||||||
|
@ -129,9 +131,9 @@ promise.then(
|
||||||
);
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
The first argument of `then` runs on `resolve` and gets the result, and the second one -- on `reject` and gets the 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 example:
|
For instance:
|
||||||
|
|
||||||
```js run
|
```js run
|
||||||
let promise = new Promise(function(resolve, reject) {
|
let promise = new Promise(function(resolve, reject) {
|
||||||
|
@ -147,6 +149,8 @@ promise.then(
|
||||||
);
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
In case of a rejection:
|
||||||
|
|
||||||
```js run
|
```js run
|
||||||
let promise = new Promise(function(resolve, reject) {
|
let promise = new Promise(function(resolve, reject) {
|
||||||
setTimeout(() => reject(new Error("Whoops!")), 1000);
|
setTimeout(() => reject(new Error("Whoops!")), 1000);
|
||||||
|
@ -182,33 +186,33 @@ let promise = new Promise((resolve, reject) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
*!*
|
*!*
|
||||||
// same as promise.then(null, alert)
|
// .catch(f) is the same as promise.then(null, f)
|
||||||
promise.catch(alert); // shows "Error: Whoops!" after 1 second
|
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"
|
````smart header="On settled promises `then` runs immediately"
|
||||||
If the promise is pending, `.then/catch` handlers wait for the result. Otherwise, if the promise has already settled, they execute 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
|
```js run
|
||||||
let promise = new Promise(resolve => resolve("done!")); // immediately resolve
|
// an immediately resolved promise
|
||||||
|
let promise = new Promise(resolve => resolve("done!"));
|
||||||
|
|
||||||
promise.then(alert); // done! (without delay)
|
promise.then(alert); // done! (without a delay)
|
||||||
```
|
```
|
||||||
|
|
||||||
That's good for jobs that may be both asynchronous (loading something) and synchronous (we already have it and can resolve right now). The handler is guaranteed to run in both cases.
|
That's good for jobs that may sometimes require time and sometimes finish immediately. The handler is guaranteed to run in both cases.
|
||||||
````
|
````
|
||||||
|
|
||||||
Now let's see more practical examples to see how promises can help us to write asynchronous code.
|
Now let's see more practical examples to see how promises can help us in writing asynchronous code.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Example: loadScript
|
## Example: loadScript
|
||||||
|
|
||||||
We have the `loadScript` function for loading a script from the previous chapter.
|
We have the `loadScript` function for loading a script from the previous chapter.
|
||||||
|
|
||||||
Here's the callback-based variant:
|
Here's the callback-based variant, just to remind it:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
function loadScript(src, callback) {
|
function loadScript(src, callback) {
|
||||||
|
@ -255,10 +259,10 @@ promise.then(script => alert('One more handler to do something else!'));
|
||||||
We can immediately see few benefits over the callback-based syntax:
|
We can immediately see few benefits over the callback-based syntax:
|
||||||
|
|
||||||
```compare minus="Callbacks" plus="Promises"
|
```compare minus="Callbacks" plus="Promises"
|
||||||
- We must have `callback` function available when calling `loadScript`. So we must know what to do with the result *before* we make a call for it.
|
- 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.
|
- 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.
|
+ 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.
|
+ We can call `.then` as many times as we want, at any point of time later.
|
||||||
```
|
```
|
||||||
|
|
||||||
So promises give us better code flow and flexibility. But there's more. We'll see that in the next chapters.
|
So promises already give us better code flow and flexibility. But there's more. We'll see that in the next chapters.
|
||||||
|
|
|
@ -1,15 +1,20 @@
|
||||||
The short answer is: **no, they are not**:
|
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:
|
The difference is that if an error happens in `f1`, then it is handled by `.catch` here:
|
||||||
|
|
||||||
```js run
|
```js run
|
||||||
promise.then(f1).catch(f2);
|
promise
|
||||||
|
.then(f1)
|
||||||
|
.catch(f2);
|
||||||
```
|
```
|
||||||
|
|
||||||
...But not here:
|
...But not here:
|
||||||
|
|
||||||
```js run
|
```js run
|
||||||
promise.then(f1, f2);
|
promise
|
||||||
|
.then(f1, f2);
|
||||||
```
|
```
|
||||||
|
|
||||||
That's because an error/result is passed down the chain, and in the second code piece there's no chain below.
|
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.
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Promise then vs catch
|
# 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?
|
Are these code fragments equal? In other words, do they behave the same way in any circumstances, for any handler functions?
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# Error in setTimeout
|
# Error in setTimeout
|
||||||
|
|
||||||
How do you think, does the `.catch` trigger? Explain, why?
|
How do you think, does the `.catch` trigger? Explain your answer?
|
||||||
|
|
||||||
```js
|
```js
|
||||||
new Promise(function(resolve, reject) {
|
new Promise(function(resolve, reject) {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
|
|
||||||
# Promises chaining
|
# Promises chaining
|
||||||
|
|
||||||
Let's formulate the problem mentioned in the chapter <info:callback-hell>:
|
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.
|
- We have a sequence of asynchronous tasks to be done one after another. For instance, loading scripts.
|
||||||
- How to code it well?
|
- How to code it well?
|
||||||
|
@ -24,7 +24,7 @@ new Promise(function(resolve, reject) {
|
||||||
alert(result); // 1
|
alert(result); // 1
|
||||||
return result * 2;
|
return result * 2;
|
||||||
|
|
||||||
}).then(function(result) {
|
}).then(function(result) { // (***)
|
||||||
|
|
||||||
alert(result); // 2
|
alert(result); // 2
|
||||||
return result * 2;
|
return result * 2;
|
||||||
|
@ -37,15 +37,40 @@ new Promise(function(resolve, reject) {
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
Here the first promise resolves in 1 second `(*)`, then the first handler is called `(**)`, its result is passed down to the second one etc. The result is passed along the chain of handlers, so we can see a sequence of `alert` calls: `1` -> `2` -> `4`.
|
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`.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
The whole thing works, because a call to `promise.then` returns a promise, so that we can call next `.then` on it, to get a new promise and so on.
|
The whole thing works, because a call to `promise.then` returns a promise, so that we can call next `.then` on it.
|
||||||
|
|
||||||
A result of a handler becomes a result of the promise returned by the corresponding `.then`.
|
When a handler returns a value, it becomes the result of that promise, so the next `.then` is called with it.
|
||||||
|
|
||||||
Please note: technically we can also add many `.then` to a single promise, without any chaining, like here:
|
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
|
```js run
|
||||||
let promise = new Promise(function(resolve, reject) {
|
let promise = new Promise(function(resolve, reject) {
|
||||||
|
@ -68,13 +93,13 @@ promise.then(function(result) {
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
...But that's a totally different thing. All `.then` on the same promise get the same result -- the result of that promise:
|
...But that's a totally different thing. Here's the picture (compare it with the chaining above):
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
So in the code above all `alert` show the same: 1. There is no result-passing between them.
|
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 chaining is used far more often than adding many handlers to the same promise.
|
In practice we rarely need multiple handlers for one promise. Chaining is used much more often.
|
||||||
|
|
||||||
## Returning promises
|
## Returning promises
|
||||||
|
|
||||||
|
@ -94,20 +119,18 @@ new Promise(function(resolve, reject) {
|
||||||
alert(result); // 1
|
alert(result); // 1
|
||||||
|
|
||||||
*!*
|
*!*
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => { // (*)
|
||||||
setTimeout(() => resolve(result * 2), 1000);
|
setTimeout(() => resolve(result * 2), 1000);
|
||||||
});
|
});
|
||||||
*/!*
|
*/!*
|
||||||
|
|
||||||
}).then(function(result) {
|
}).then(function(result) { // (**)
|
||||||
|
|
||||||
alert(result); // 2
|
alert(result); // 2
|
||||||
|
|
||||||
*!*
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
setTimeout(() => resolve(result * 2), 1000);
|
setTimeout(() => resolve(result * 2), 1000);
|
||||||
});
|
});
|
||||||
*/!*
|
|
||||||
|
|
||||||
}).then(function(result) {
|
}).then(function(result) {
|
||||||
|
|
||||||
|
@ -116,15 +139,15 @@ new Promise(function(resolve, reject) {
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
Here each `.then` returns `new Promise(…)`. When it settles, the result is passed on.
|
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 with 1 second delay between `alert` calls.
|
So the output is again 1 -> 2 > 4, but now with 1 second delay between `alert` calls.
|
||||||
|
|
||||||
That feature allows to build chains of asynchronous actions.
|
Returning promises allows us to build chains of asynchronous actions.
|
||||||
|
|
||||||
## Example: loadScript
|
## Example: loadScript
|
||||||
|
|
||||||
Let's use it with `loadScript` to load scripts one by one, sequentially:
|
Let's use this feature with `loadScript` to load scripts one by one, in sequence:
|
||||||
|
|
||||||
```js run
|
```js run
|
||||||
loadScript("/article/promise-chaining/one.js")
|
loadScript("/article/promise-chaining/one.js")
|
||||||
|
@ -143,273 +166,156 @@ loadScript("/article/promise-chaining/one.js")
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
Here each `loadScript` call returns a promise, and the next `.then` awaits until it resolves. So scripts are loaded one after another.
|
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, and the code is still "flat", no signs of "pyramid of doom".
|
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".
|
||||||
|
|
||||||
## Error handling
|
Please note that technically it is also possible to write `.then` directly after each promise, without returning them, like this:
|
||||||
|
|
||||||
Asynchronous actions may sometimes fail. For instance, `loadScript` fails if there's no such script. In that case the promise becomes rejected, so we can use `.catch` to handle it.
|
|
||||||
|
|
||||||
Luckily, chaining is great for catching errors. When a promise rejects, the control jumps to the closest rejection handler down the chain. That's very convenient.
|
|
||||||
|
|
||||||
In the example below we append `.catch` to handle all errors in the scripts loading chain:
|
|
||||||
|
|
||||||
```js run
|
```js run
|
||||||
*!*
|
loadScript("/article/promise-chaining/one.js").then(function(script1) {
|
||||||
loadScript("/article/promise-chaining/ERROR.js")
|
loadScript("/article/promise-chaining/two.js").then(function(script2) {
|
||||||
*/!*
|
loadScript("/article/promise-chaining/three.js").then(function(script3) {
|
||||||
.then(function(script) {
|
// this function has access to variables script1, script2 and script3 (*)
|
||||||
return loadScript("/article/promise-chaining/two.js");
|
one();
|
||||||
})
|
two();
|
||||||
.then(function(script) {
|
three();
|
||||||
return loadScript("/article/promise-chaining/three.js");
|
});
|
||||||
})
|
|
||||||
.then(function(script) {
|
|
||||||
alert('done!');
|
|
||||||
})
|
|
||||||
*!*
|
|
||||||
.catch(function(error) { // (*)
|
|
||||||
alert(error.message);
|
|
||||||
});
|
});
|
||||||
*/!*
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
In the code above the first `loadScript` call fails, because `ERROR.js` doesn't exist. The control jumps to the closest error handler `(*)`.
|
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.
|
||||||
|
|
||||||
In the example below the second script fails to load. Please note that the same `.catch` handles it, just because it's the closest one down the chain:
|
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.
|
||||||
|
|
||||||
|
## Bigger example: fetch
|
||||||
|
|
||||||
|
In frontend programming promises are often used for network requests. So let's make an 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);
|
||||||
|
```
|
||||||
|
|
||||||
|
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 with the full response text when it's downloaded from the remote server.
|
||||||
|
|
||||||
|
The code below makes a request to `user.json` and then loads it as text from the server:
|
||||||
|
|
||||||
```js run
|
```js run
|
||||||
loadScript("/article/promise-chaining/one.js")
|
fetch('/article/promise-chaining/user.json')
|
||||||
.then(function(script) {
|
// .then runs when the remote server responds
|
||||||
*!*
|
.then(function(response) {
|
||||||
return loadScript("/article/promise-chaining/ERROR.js");
|
// response.text() is the new promise that resolves when the server finishes sending data
|
||||||
*/!*
|
return response.text();
|
||||||
})
|
})
|
||||||
.then(function(script) {
|
.then(function(text) {
|
||||||
return loadScript("/article/promise-chaining/three.js");
|
// ...and here's the content of the remote file
|
||||||
})
|
alert(text); // {"name": "iliakan", isAdmin: true}
|
||||||
.then(function(script) {
|
|
||||||
alert('done!');
|
|
||||||
})
|
|
||||||
*!*
|
|
||||||
.catch(function(error) {
|
|
||||||
alert(error.message);
|
|
||||||
});
|
});
|
||||||
*/!*
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The `.catch` works like in a `try..catch` block. 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.
|
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:
|
||||||
## Implicit try..catch
|
|
||||||
|
|
||||||
The code inside the executor and handlers has something like an invisible `try..catch` around it. If an error happens, it's considered a rejection.
|
|
||||||
|
|
||||||
For instance, this code:
|
|
||||||
|
|
||||||
```js run
|
```js run
|
||||||
new Promise(function(resolve, reject) {
|
fetch('/article/promise-chaining/user.json')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(user => alert(user.name)); // iliakan
|
||||||
|
```
|
||||||
|
|
||||||
|
Now let's do something with it. For instance, we can make one more request to github, load the user profile and show its 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); // (*)
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Now it works. But there's a potential problem in the code.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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())
|
||||||
*!*
|
*!*
|
||||||
throw new Error("Whoops!");
|
.then(githubUser => new Promise(function(resolve, reject) {
|
||||||
*/!*
|
let img = document.createElement('img');
|
||||||
}).catch(function(error) {
|
img.src = githubUser.avatar_url;
|
||||||
alert(error.message); // Whoops!
|
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}`));
|
||||||
```
|
```
|
||||||
|
|
||||||
...Works the same way as this:
|
An async action should always return a promise. That makes possible to plan actions after it.
|
||||||
|
|
||||||
|
Finally, we can split it into reusable functions:
|
||||||
|
|
||||||
```js run
|
```js run
|
||||||
new Promise(function(resolve, reject) {
|
function loadJson(url) {
|
||||||
*!*
|
return fetch(url)
|
||||||
reject(new Error("Whoops!"));
|
.then(response => response.json());
|
||||||
*/!*
|
|
||||||
}).catch(function(error) {
|
|
||||||
alert(error.message); // Whoops!
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
The `Promise` constructor automatically catches the error and treats it as a rejection.
|
|
||||||
|
|
||||||
That works 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) {
|
|
||||||
*!*
|
|
||||||
// .then returns a rejected promise
|
|
||||||
throw new Error("Whoops!");
|
|
||||||
*/!*
|
|
||||||
}).catch(function(error) {
|
|
||||||
// and the error is handled here
|
|
||||||
alert(error.message); // 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(function(error) {
|
|
||||||
alert(error.message); // blabla is not defined
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
To summarize, a promise handler can finish in three ways:
|
|
||||||
|
|
||||||
1. It can return a value (or undefined if there's no `return`). Then the promise returned by `.then` becomes fulfilled, and the next handler is called with that value.
|
|
||||||
2. It can throw an error. Then the promise returned by `.then` becomes rejected and the closest rejection handler is called.
|
|
||||||
3. It can return a promise. Then JavaScript awaits its result and goes on with it.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## 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 finish normally, then it continues to the closest successful `.then` handler.
|
|
||||||
|
|
||||||
In the example below the error is fully handled, and the execution continues normally:
|
|
||||||
|
|
||||||
```js run
|
|
||||||
// the execution: catch -> then
|
|
||||||
new Promise(function(resolve, reject) {
|
|
||||||
|
|
||||||
throw new Error("Whoops!");
|
|
||||||
|
|
||||||
}).catch(function(error) {
|
|
||||||
|
|
||||||
alert("Handled it!");
|
|
||||||
*!*
|
|
||||||
return "result"; // return, the execution goes the "normal way"
|
|
||||||
*/!*
|
|
||||||
|
|
||||||
*!*
|
|
||||||
}).then(alert); // result shown
|
|
||||||
*/!*
|
|
||||||
```
|
|
||||||
|
|
||||||
...And here's an example of "rethrowing":
|
|
||||||
|
|
||||||
|
|
||||||
```js run
|
|
||||||
// the execution: catch -> catch -> then
|
|
||||||
new Promise(function(resolve, reject) {
|
|
||||||
|
|
||||||
throw new Error("Whoops!");
|
|
||||||
|
|
||||||
}).catch(function(error) { // (*)
|
|
||||||
|
|
||||||
alert("Can't handle!");
|
|
||||||
|
|
||||||
*!*
|
|
||||||
throw error; // throwing this or another error jumps to the next catch
|
|
||||||
*/!*
|
|
||||||
|
|
||||||
}).catch(error => {
|
|
||||||
|
|
||||||
alert("Trying to handle again...");
|
|
||||||
// don't return anything => execution goes the normal way
|
|
||||||
|
|
||||||
}).then(alert); // undefined
|
|
||||||
```
|
|
||||||
|
|
||||||
The handler `(*)` catches the error. In real project it would try to handle it somehow, but here it just throws it again. So the execution jumps to the next `.catch` down the chain.
|
|
||||||
|
|
||||||
## Unhandled rejections
|
|
||||||
|
|
||||||
...But what if we forget to append an error handler to the end of the chain?
|
|
||||||
|
|
||||||
Like here:
|
|
||||||
|
|
||||||
```js untrusted run refresh
|
|
||||||
new Promise(function() {
|
|
||||||
errorHappened(); // 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!
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
Technically, when an error happens, the promise state becomes "rejected", and the execution should jump to the closest rejection handler. But there is no such handler in the examples above.
|
|
||||||
|
|
||||||
Usually that means that the code is bad. Most JavaScript engines track such situations and generate a global error. In the browser we can catch it using `window.addEventListener('unhandledrejection')` (as specified in the [HTML standard](https://html.spec.whatwg.org/multipage/webappapis.html#unhandled-promise-rejections)):
|
|
||||||
|
|
||||||
|
|
||||||
```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!");
|
|
||||||
}).then(function() {
|
|
||||||
// ...something...
|
|
||||||
}).then(function() {
|
|
||||||
// ...something else...
|
|
||||||
}).then(function() {
|
|
||||||
// ...but no catch after it!
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
In non-browser environments there are also a similar events, so we can always track unhandled errors in promises.
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
|
|
||||||
- A call to `.then/catch` returns a promise, so we can chain them.
|
|
||||||
- There are 3 possible outcomes of a handler:
|
|
||||||
1. Return normally -- the result is passed to the closest successful handler down the chain.
|
|
||||||
2. Throw an error -- it is passed to the closest rejection handler down the chain.
|
|
||||||
3. Return a promise -- the chain waits till it settles, and then its result is used.
|
|
||||||
|
|
||||||
That allows to put a "queue" of actions into the chain.
|
|
||||||
|
|
||||||
Here's a more complex example. The function `showMessage` loads a message and shows it:
|
|
||||||
|
|
||||||
```js run
|
|
||||||
function showMessage() {
|
|
||||||
return new Promise(function(resolve, reject) {
|
|
||||||
alert('loading...');
|
|
||||||
return loadScript('/article/promise-chaining/getMessage.js');
|
|
||||||
}).then(function(script) {
|
|
||||||
let message = getMessage(); // getMessage function comes from the script
|
|
||||||
return animateMessage(message);
|
|
||||||
}).catch(function(err) { /*...*/ });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function animateMessage(message) {
|
function loadGithubUser(name) {
|
||||||
|
return fetch(`https://api.github.com/users/${name}`)
|
||||||
|
.then(response => response.json());
|
||||||
|
}
|
||||||
|
|
||||||
|
function showAvatar(githubUser) {
|
||||||
return new Promise(function(resolve, reject) {
|
return new Promise(function(resolve, reject) {
|
||||||
// should be asynchronous animation
|
let img = document.createElement('img');
|
||||||
alert(message);
|
img.src = githubUser.avatar_url;
|
||||||
resolve();
|
img.className = "promise-avatar-example";
|
||||||
|
document.body.append(img);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
img.remove();
|
||||||
|
resolve(githubUser);
|
||||||
|
}, 3000);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
showMessage();
|
loadJson('/article/promise-chaining/user.json')
|
||||||
|
.then(user => loadGithubUser(user.name))
|
||||||
|
.then(showAvatar)
|
||||||
|
.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.
|
||||||
|
|
||||||
|
Here we assumed that everything works as intended. But that's not always the case. In the next chapter we'll talk about error handling.
|
||||||
|
|
|
@ -11,3 +11,12 @@ function loadScript(src) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.promise-avatar-example {
|
||||||
|
border-radius: 50%;
|
||||||
|
position: fixed;
|
||||||
|
left: 10px;
|
||||||
|
top: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
BIN
8-async/03-promise-chaining/promise-handler-variants-2.png
Normal file
After Width: | Height: | Size: 24 KiB |
BIN
8-async/03-promise-chaining/promise-handler-variants-2@2x.png
Normal file
After Width: | Height: | Size: 56 KiB |
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 35 KiB |
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 81 KiB |
|
@ -1,15 +0,0 @@
|
||||||
# Promises API
|
|
||||||
|
|
||||||
Let's meet more functions and methods for promises.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Keywords `async` and `await` provide a more elegant way to write the code using promises.
|
|
||||||
|
|
||||||
## Async functions
|
|
||||||
|
|
||||||
The `async` function is like a regular one, but it wraps a returned value in a `Promise`.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Nowadays, promises are de-facto standard for asynchronous actions, when we need to
|
|
287
8-async/04-promise-error/article.md
Normal file
|
@ -0,0 +1,287 @@
|
||||||
|
# Error handling
|
||||||
|
|
||||||
|
Asynchronous actions may sometimes fail: errors are possible, so corresponding promises can become rejected. For instance, `loadScript` fails if there's no such script. 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 the example below we append `.catch` to handle all errors in the scripts loading chain:
|
||||||
|
|
||||||
|
```js run
|
||||||
|
*!*
|
||||||
|
loadScript("NO_SUCH_SCRIPT.js")
|
||||||
|
*/!*
|
||||||
|
.then(function(script) {
|
||||||
|
return loadScript("/article/promise-chaining/two.js");
|
||||||
|
})
|
||||||
|
.then(function(script) {
|
||||||
|
return loadScript("/article/promise-chaining/three.js");
|
||||||
|
})
|
||||||
|
.then(function(script) {
|
||||||
|
alert('done!');
|
||||||
|
})
|
||||||
|
*!*
|
||||||
|
.catch(function(error) { // (*)
|
||||||
|
alert(error.message);
|
||||||
|
});
|
||||||
|
*/!*
|
||||||
|
```
|
||||||
|
|
||||||
|
In the code above the first `loadScript` call fails, because `NO_SUCH_SCRIPT.js` doesn't exist. The control jumps to the closest error handler `(*)`.
|
||||||
|
|
||||||
|
Now let's see what happens if the second script fails to load. Actually, the same `.catch` handles it, just because it's the closest one down the chain:
|
||||||
|
|
||||||
|
|
||||||
|
```js run
|
||||||
|
loadScript("/article/promise-chaining/one.js")
|
||||||
|
.then(function(script) {
|
||||||
|
*!*
|
||||||
|
return loadScript("NO_SUCH_SCRIPT.js");
|
||||||
|
*/!*
|
||||||
|
})
|
||||||
|
.then(function(script) {
|
||||||
|
return loadScript("/article/promise-chaining/three.js");
|
||||||
|
})
|
||||||
|
.then(function(script) {
|
||||||
|
alert('done!');
|
||||||
|
})
|
||||||
|
*!*
|
||||||
|
.catch(function(error) {
|
||||||
|
alert(error.message);
|
||||||
|
});
|
||||||
|
*/!*
|
||||||
|
```
|
||||||
|
|
||||||
|
The `.catch` works similarly to the `try..catch` construct. 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.
|
||||||
|
|
||||||
|
|
||||||
|
## 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(function(error) {
|
||||||
|
alert(error.message); // Whoops!
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
...Works the same way as this:
|
||||||
|
|
||||||
|
```js run
|
||||||
|
new Promise(function(resolve, reject) {
|
||||||
|
*!*
|
||||||
|
reject(new Error("Whoops!"));
|
||||||
|
*/!*
|
||||||
|
}).catch(function(error) {
|
||||||
|
alert(error.message); // Whoops!
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
The "invisible `try..catch`" around the executor automatically catches the error and treats it as a rejection.
|
||||||
|
|
||||||
|
That works 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) {
|
||||||
|
*!*
|
||||||
|
// .then returns a rejected promise
|
||||||
|
throw new Error("Whoops!");
|
||||||
|
*/!*
|
||||||
|
}).catch(function(error) {
|
||||||
|
// and the error is handled here
|
||||||
|
alert(error.message); // 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(function(error) {
|
||||||
|
alert(error.message); // blabla is not defined
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## The full picture
|
||||||
|
|
||||||
|
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 for its result and goes on with it as described above.
|
||||||
|
|
||||||
|
The picture of how the promise returned by `.then/catch` changes:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
The smaller picture of how handlers are called:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 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` handles the error and finishes with `return`, so the execution continues normally, the control goes to the next `.then`:
|
||||||
|
|
||||||
|
```js run
|
||||||
|
// the execution: catch -> then
|
||||||
|
new Promise(function(resolve, reject) {
|
||||||
|
|
||||||
|
throw new Error("Whoops!");
|
||||||
|
|
||||||
|
}).catch(function(error) {
|
||||||
|
|
||||||
|
alert("Handled it!");
|
||||||
|
*!*
|
||||||
|
return "result"; // return, the execution goes the "normal way"
|
||||||
|
*/!*
|
||||||
|
|
||||||
|
*!*
|
||||||
|
}).then(alert); // result shown
|
||||||
|
*/!*
|
||||||
|
```
|
||||||
|
|
||||||
|
...And here's an example of "rethrowing":
|
||||||
|
|
||||||
|
|
||||||
|
```js run
|
||||||
|
// the execution: catch -> catch -> then
|
||||||
|
new Promise(function(resolve, reject) {
|
||||||
|
|
||||||
|
throw new Error("Whoops!");
|
||||||
|
|
||||||
|
}).catch(function(error) { // (*)
|
||||||
|
|
||||||
|
alert("Can't handle the error here!");
|
||||||
|
|
||||||
|
*!*
|
||||||
|
throw error; // throwing this or another error jumps to the next catch
|
||||||
|
*/!*
|
||||||
|
|
||||||
|
}).catch(error => { // (**)
|
||||||
|
|
||||||
|
alert("The error is fully handled.");
|
||||||
|
// don't return anything => execution goes the normal way
|
||||||
|
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
The handler `(*)` catches the error and just can't handle it. In real project it would try to analyze the error object, but here it just throws it again. So the execution jumps to the next `.catch` down the chain `(**)`.
|
||||||
|
|
||||||
|
## Unhandled rejections
|
||||||
|
|
||||||
|
...But what if we forget to append an error handler to the end of the chain?
|
||||||
|
|
||||||
|
Like here:
|
||||||
|
|
||||||
|
```js untrusted run refresh
|
||||||
|
new Promise(function() {
|
||||||
|
errorHappened(); // 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!
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Technically, when an error happens, the promise state becomes "rejected", and the execution should jump to the closest rejection handler. But there is no such handler in the examples above.
|
||||||
|
|
||||||
|
Usually 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 `window.addEventListener('unhandledrejection')` as specified in the [HTML standard](https://html.spec.whatwg.org/multipage/webappapis.html#unhandled-promise-rejections):
|
||||||
|
|
||||||
|
|
||||||
|
```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!");
|
||||||
|
}).then(function() {
|
||||||
|
// ...something...
|
||||||
|
}).then(function() {
|
||||||
|
// ...something else...
|
||||||
|
}).then(function() {
|
||||||
|
// ...but no catch after it!
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Now if an error has occured, and there's no `.catch`, the event `unhandledrejection` triggers, and our handler can do something with the exception. Once again, such situation is usually a programming error.
|
||||||
|
|
||||||
|
In non-browser environments like Node.JS there are other similar ways to track unhandled errors.
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
- A call to `.then/catch` returns a promise, so we can chain them.
|
||||||
|
- There are 3 possible outcomes of a handler:
|
||||||
|
1. Return normally -- the result is passed to the closest successful handler down the chain.
|
||||||
|
2. Throw an error -- it is passed to the closest rejection handler down the chain.
|
||||||
|
3. Return a promise -- the chain waits till it settles, and then its result is used.
|
||||||
|
|
||||||
|
That allows to form a "queue" of actions in the chain.
|
||||||
|
|
||||||
|
The great thing about chaining is that we can always append more actions to it.
|
||||||
|
|
||||||
|
For instance, the `showMessage()` function below loads a script, and then animates a message:
|
||||||
|
|
||||||
|
```js run
|
||||||
|
function showMessage() {
|
||||||
|
return new Promise(function(resolve, reject) {
|
||||||
|
alert('loading...');
|
||||||
|
return loadScript('/article/promise-chaining/getMessage.js');
|
||||||
|
}).then(function(script) {
|
||||||
|
let message = getMessage(); // getMessage function comes from the script
|
||||||
|
return animateMessage(message);
|
||||||
|
}).catch(function(err) { /*...*/ });
|
||||||
|
}
|
||||||
|
|
||||||
|
function animateMessage(message) {
|
||||||
|
return new Promise(function(resolve, reject) {
|
||||||
|
// ...imagine the code for an asynchronous CSS-animation here...
|
||||||
|
alert(message);
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
showMessage();
|
||||||
|
```
|
||||||
|
|
||||||
|
We can add more actions like this:
|
||||||
|
```js
|
||||||
|
showMessage()
|
||||||
|
.then(function() {
|
||||||
|
// the code to run after the message is shown
|
||||||
|
});
|
||||||
|
```
|
53
8-async/05-promise-api/article.md
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
# Promise API
|
||||||
|
|
||||||
|
There are helpful static methods in the `Promise` class. There are only 4 of them, so we'll quickly cover them here.
|
||||||
|
|
||||||
|
## 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, for instance an array and returns a new promise that resolves with the array of their results when all of them are settled, or rejects with the first encountered error if any.
|
||||||
|
|
||||||
|
For instance:
|
||||||
|
|
||||||
|
```js run
|
||||||
|
// loads 3 scripts in parallel and returns an array of them
|
||||||
|
Promise.all([
|
||||||
|
loadScript('/article/promise-api/one.js'),
|
||||||
|
loadScript('/article/promise-api/two.js'),
|
||||||
|
loadScript('/article/promise-api/three.js')
|
||||||
|
]).then(scripts => {
|
||||||
|
alert(`scripts loaded: ${scripts}`);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
- the returned `promise` awaits for al
|
||||||
|
|
||||||
|
In the previous chapter we saw how to run things sequentially. Promises also
|
||||||
|
|
||||||
|
- Promise.all
|
||||||
|
- Promise.race
|
||||||
|
- Promise.reject
|
||||||
|
- Promise.resolve
|
||||||
|
|
||||||
|
Let's meet more functions and methods for promises.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Keywords `async` and `await` provide a more elegant way to write the code using promises.
|
||||||
|
|
||||||
|
## Async functions
|
||||||
|
|
||||||
|
The `async` function is like a regular one, but it wraps a returned value in a `Promise`.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Nowadays, promises are de-facto standard for asynchronous actions, when we need to
|
13
8-async/05-promise-api/head.html
Normal 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>
|
3
8-async/05-promise-api/one.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
function one() {
|
||||||
|
alert(1);
|
||||||
|
}
|
3
8-async/05-promise-api/three.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
function three() {
|
||||||
|
alert(3);
|
||||||
|
}
|
3
8-async/05-promise-api/two.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
function two() {
|
||||||
|
alert(2);
|
||||||
|
}
|
|
@ -2,4 +2,4 @@ development: true
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# Promises, async/await
|
# The art of async programming: promises, async/await
|
||||||
|
|