Skip to content

Commit c968817

Browse files
committed
minor
1 parent 2f4242c commit c968817

File tree

11 files changed

+408
-0
lines changed

11 files changed

+408
-0
lines changed
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
2+
# Callback hell
3+
4+
Many things that we do in JavaScript are asynchronous. We initiate a process, but it finishes later.
5+
6+
The most obvious example is `setTimeout`, but there are others, like making network requests, performing animations and so on.
7+
8+
Let's see a couple of examples, so that we can discover a problem, and then solve it using "promises".
9+
10+
[cut]
11+
12+
## Callbacks
13+
14+
Remember resource load/error events? They are covered in the chapter <info:onload-onerror>.
15+
16+
Let's say we want to create a function `loadScript` that loads a script and executes our code afterwards.
17+
18+
It can look like this:
19+
20+
```js
21+
function loadScript(src, callback) {
22+
let script = document.createElement('script');
23+
script.src = src;
24+
25+
script.onload = () => callback(script);
26+
27+
document.head.append(script);
28+
}
29+
```
30+
31+
Now when we want to load a script and then do something, we can call:
32+
33+
```js
34+
loadScript('/my/script.js', function(script) {
35+
alert(`Cool, the ${script.src} is loaded, let's use it`);
36+
});
37+
```
38+
39+
...And it works, shows `alert` after the script is loaded.
40+
41+
That is called "a callback API". Our function `loadScript` performs an asynchronous task and we can hook on its completion using the callback function.
42+
43+
## Callback in callback
44+
45+
What if we need to load two scripts: one more after the first one?
46+
47+
We can put another `loadScript` inside the callback, like this:
48+
49+
```js
50+
loadScript('/my/script.js', function(script) {
51+
alert(`Cool, the ${script.src} is loaded, let's load one more`);
52+
53+
loadScript('/my/script2.js', function(script) {
54+
55+
alert(`Cool, the second script is loaded`);
56+
57+
});
58+
59+
});
60+
```
61+
62+
Now after the outer `loadScript` is complete, the callback initiates the inner one.
63+
64+
...What if we want one more script?
65+
66+
```js
67+
loadScript('/my/script.js', function(script) {
68+
69+
loadScript('/my/script2.js', function(script) {
70+
71+
if (something) {
72+
loadScript('/my/script3.js', function(script) {
73+
*!*
74+
// ...continue after all scripts are loaded
75+
*/!*
76+
});
77+
}
78+
79+
})
80+
81+
});
82+
```
83+
84+
As you can see, a new asynchronous action means one more nesting level.
85+
86+
## Handling errors
87+
88+
In this example we didn't consider errors. What if a script loading failed with an error? Our callback should be able to react on that.
89+
90+
Here's an improved version of `loadScript` that can handle errors:
91+
92+
```js run
93+
function loadScript(src, callback) {
94+
let script = document.createElement('script');
95+
script.src = src;
96+
97+
*!*
98+
script.onload = () => callback(null, script);
99+
script.onerror = () => callback(new Error(`Script load error ` + src));
100+
*/!*
101+
102+
document.head.append(script);
103+
}
104+
```
105+
106+
It calls `callback(null, script)` for successful load and `callback(error)` otherwise.
107+
108+
Usage:
109+
```js
110+
loadScript('/my/script.js', function(error, script) {
111+
if (error) {
112+
// handle error
113+
} else {
114+
// script loaded, go on
115+
}
116+
});
117+
```
118+
119+
The first argument of `callback` is reserved for errors and the second argument for the successful result. That allows to use a single function to pass both success and failure.
120+
121+
## Pyramid of doom
122+
123+
From the first look it's a viable code. And indeed it is. For one or maybe two nested calls it looks fine.
124+
125+
But for multiple asynchronous actions that follow one after another...
126+
127+
```js
128+
loadScript('/my/script1.js', function(error, script) {
129+
130+
if (error) {
131+
handleError(error);
132+
} else {
133+
// ...
134+
loadScript('/my/script2.js', function(error, script) {
135+
if (error) {
136+
handleError(error);
137+
} else {
138+
// ...
139+
loadScript('/my/script3.js', function(error, script) {
140+
if (error) {
141+
handleError(error);
142+
} else {
143+
*!*
144+
// ...continue after all scripts are loaded (*)
145+
*/!*
146+
}
147+
});
148+
149+
}
150+
})
151+
}
152+
});
153+
```
154+
155+
In the code above:
156+
1. we load `/my/script1.js`, then if there's no error
157+
2. we load `/my/script2.js`, then if there's no error
158+
3. we load `/my/script3.js`, then if there's no error -- do something else `(*)`.
159+
160+
The nested calls become 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.
161+
162+
That's sometimes called "callback hell" or "pyramid of doom".
163+
164+
![](callback-hell.png)
165+
166+
See? It grows right with every asynchronous action.
167+
168+
Compare that with a "regular" synchronous code.
169+
170+
Just *if* `loadScript` were a regular synchronous function:
171+
172+
```js
173+
try {
174+
// assume we get the result in a synchronous manner, without callbacks
175+
let script = loadScript('/my/script.js');
176+
// ...
177+
let script2 = loadScript('/my/script2.js');
178+
// ...
179+
let script3 = loadScript('/my/script3.js');
180+
} catch(err) {
181+
handleError(err);
182+
}
183+
```
184+
185+
How much cleaner and simpler it is!
186+
187+
Promises allow to write asynchronous code in a similar way. They are really great at that. Let's study them in the next chapter.
33.1 KB
Loading
78.7 KB
Loading
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
# Promise
2+
3+
There are many ways to explain promises. Here we'll follow the [specification](https://tc39.github.io/ecma262/#sec-promise-objects), because gives real understanding how things work. Besides, you'll become familiar with the terms.
4+
5+
First, what a promise is, and then usage patterns.
6+
7+
[cut]
8+
9+
## Promise objects
10+
11+
A promise is an object of the built-in `Promise` class. It has the meaning of the "delayed result".
12+
13+
The constructor syntax is:
14+
15+
```js
16+
let promise = new Promise(function(resolve, reject) {
17+
// ...
18+
});
19+
```
20+
21+
The function is called *executor*. It is called automatically when the promise is created and can do an asynchronous job, like loading a script, but can be something else.
22+
23+
At the end, it should call one of:
24+
25+
- `resolve(result)` -- to indicate that the job finished successfully with the `result`.
26+
- `reject(error)` -- to indicate that an error occured, and `error` should be the `Error` object (technically can be any value).
27+
28+
For instance:
29+
30+
```js
31+
let promise = new Promise(function(*!*resolve*/!*, reject) {
32+
// this function is executed automatically when the promise is constructed
33+
setTimeout(() => *!*resolve("done!")*/!*, 1000);
34+
});
35+
```
36+
37+
Or, in case of an error:
38+
39+
```js
40+
let promise = new Promise(function(resolve, *!*reject*/!*) {
41+
setTimeout(() => *!*reject(new Error("Woops!"))*/!*, 1000);
42+
});
43+
```
44+
45+
Initially, the promise is said to have a "pending" state. Then it has two ways. When `resolve` is called, the state becomes "fulfilled". When `reject` is called, the state becomes "rejected":
46+
47+
![](promiseInit.png)
48+
49+
The idea of promises is that an external code may react on the state change.
50+
51+
The `promise` object provides the method `promise.then(onResolve, onReject)`.
52+
53+
Both its arguments are functions:
54+
55+
- `onResolve` is called when the state becomes "fulfilled" and gets the result.
56+
- `onReject` is called when the state becomes "rejected" and gets the error.
57+
58+
For instance, here `promise.then` outputs the result when it comes:
59+
60+
```js run
61+
let promise = new Promise(function(resolve, reject) {
62+
setTimeout(() => resolve("done!"), 1000);
63+
});
64+
65+
// shows "done!" after 1 second
66+
promise.then(result => alert(result));
67+
```
68+
69+
...And here it shows the error message:
70+
71+
72+
```js run
73+
let promise = new Promise(function(resolve, reject) {
74+
setTimeout(() => reject(new Error("Woops!")), 1000);
75+
});
76+
77+
// shows "Woops!" after 1 second
78+
promise.then(null, error => alert(error.message));
79+
```
80+
81+
![](promise-resolve-reject.png)
82+
83+
84+
- To handle only a result, we can use single argument: `promise.then(onResolve)`
85+
- To handle only an error, we can use a shorthand method `promise.catch(onReject)` -- it's the same as `promise.then(null, onReject)`.
86+
87+
Here's the example rewritten with `promise.catch`:
88+
89+
```js run
90+
let promise = new Promise(function(resolve, reject) {
91+
setTimeout(() => reject(new Error("Woops!")), 1000);
92+
});
93+
94+
*!*
95+
promise.catch(error => alert(error.message));
96+
*/!*
97+
```
98+
99+
"What's the benefit?" -- one might ask, "How do we use it?" Let's see an example.
100+
101+
## Example: loadScript
102+
103+
We have the `loadScript` function for loading a script from the previous chapter, here's the callback-based variant:
104+
105+
```js
106+
function loadScript(src, callback) {
107+
let script = document.createElement('script');
108+
script.src = src;
109+
110+
script.onload = () => callback(null, script);
111+
script.onerror = () => callback(new Error(`Script load error ` + src));
112+
113+
document.head.append(script);
114+
}
115+
```
116+
117+
Let's rewrite it using promises.
118+
119+
The call to `loadScript(src)` below returns a promise that settles when the loading is complete:
120+
121+
```js run
122+
function loadScript(src) {
123+
return new Promise(function(resolve, reject) {
124+
let script = document.createElement('script');
125+
script.src = src;
126+
127+
script.onload = () => resolve(script);
128+
script.onerror = () => reject(new Error("Script load error: " + src));
129+
130+
document.head.append(script);
131+
});
132+
}
133+
134+
*!*
135+
// Usage:
136+
*/!*
137+
let promise = loadScript("https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js");
138+
promise.then(
139+
script => alert(`${script.src} is loaded!`),
140+
error => alert(`Error: ${error.message}`);
141+
);
142+
```
143+
144+
The benefits compared to the callback syntax:
145+
146+
- We can add as many `.then` as we want and when we want. Maybe later.
147+
- Also we can pass a promise object somewhere else, and new handlers can be added there, so that's extensible.
148+
149+
So promises already give us flexibility. But there's more. We can chain promises, see the next chapter.
150+
151+
152+
````warn header="Once a promise settles, it can't be changed"
153+
When either `resolve` or `reject` is called -- the promise becomes *settled* (fulfilled or rejected), and the state is final. The result is saved in the promise object.
154+
155+
Future calls of `resolve/reject` are ignored, there's no way to "re-resolve" or "re-reject" a promise.
156+
157+
For instance, here only the first `resolve` works:
158+
159+
```js run
160+
let promise = new Promise(function(resolve, reject) {
161+
resolve("done!"); // immediately fulfill with the result: "done"
162+
163+
setTimeout(() => resolve("..."), 1000); // ignored
164+
setTimeout(() => reject(new Error("..."), 2000)); // ignored
165+
});
166+
167+
// the promise is already fulfilled, so the alert shows up right now
168+
promise.then(result => alert(result), () => alert("Never runs"));
169+
````
170+
171+
172+
```smart header="On settled promises `then` runs immediately"
173+
As we've seen from the example above, a promise may call resolve/reject without delay. That happens sometimes, if it turns out that there's no asynchronous things to be done.
174+
175+
When the promise is settled (resolved or rejected), subsequent `promise.then` callbacks are executed immediately.
176+
```
177+
178+
````smart header="Functions resolve/reject have only one argument"
179+
Functions `resolve/reject` accept only one argument.
180+
181+
We can call them without any arguments: `resolve()`, that makes the result `undefined`:
182+
183+
```js run
184+
let promise = new Promise(function(resolve, reject) {
185+
resolve();
186+
});
187+
188+
promise.then(result => alert(result)); // undefined
189+
```
190+
191+
...But if we pass more arguments: `resolve(1, 2, 3)`, then all arguments after the first one are ignored. A promise may have only one value as a result/error.
192+
193+
Use objects and destructuring if you need to pass many values, like this:
194+
195+
```js run
196+
let promise = new Promise(function(resolve, reject) {
197+
resolve({name: "John", age: 25}); // two values packed in an object
198+
});
199+
200+
// destructuring the object into name and age variables
201+
promise.then(function({name, age}) {
202+
alert(`${name} ${age}`); // John 25
203+
})
204+
```
205+
206+
````
10.8 KB
Loading
24.8 KB
Loading
15.8 KB
Loading
37.8 KB
Loading

8-async/03-promise-chaining/article.md

Whitespace-only changes.

0 commit comments

Comments
 (0)