Skip to content

Commit 248eaeb

Browse files
committed
Promise constructors
1 parent 0203933 commit 248eaeb

File tree

1 file changed

+213
-12
lines changed

1 file changed

+213
-12
lines changed

manuscript/10-Promises.md

Lines changed: 213 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,19 @@ W> This chapter is a work-in-progress. As such, it may have more typos or conten
44

55
One of the most powerful aspects of JavaScript is how easy it handles asynchronous programming. Since JavaScript originated as a language for the web, it was a requirement to be able to respond to user interactions such as clicks and key presses. Node.js further popularized asynchronous programming in JavaScript by using callbacks as an alternative to events. As more and more programs started using asynchronous programming, there was a growing sense that these two models, events and callbacks, weren't powerful enough to support everything that developers wanted to do. Promises are the solution to this problem.
66

7-
Promises are another option for asynchronous programming, and similar functionality is available in other languages under names such as futures and deferreds. The basic idea is to specify some code to be executed later (as with events and callbacks) and also explicitly indicate if the code succeeded or failed in its task. In that way, you can chain promises together based on success or failure in ways that are easier to understand and debug.
7+
Promises are another option for asynchronous programming, and similar functionality is available in other languages under names such as futures and deferreds. The basic idea is to specify some code to be executed later (as with events and callbacks) and also explicitly indicate if the code succeeded or failed in its job. In that way, you can chain promises together based on success or failure in ways that are easier to understand and debug.
88

99
Before you can get a good understanding of how promises work, however, it's important to understand some of the basic concepts upon which they are built.
1010

1111
## Asynchronous Programming Background
1212

1313
JavaScript engines are built on the concept of a single-threaded event loop. Single-threaded means that only one piece of code is executed at any given in point in time. This stands in contrast to other languages such as Java or C++ that may use threads to allow multiple different pieces of code to execute at the same time. Maintaining, and protecting, state when multiple pieces of code can access and change that state is a difficult problem and the source of frequent bugs in thread-based software.
1414

15-
Because JavaScript engines can only execute one piece of code at a time, it's necessary to keep track of code that is meant to run. That code is kept in a *task queue*. Whenever a piece of code is ready to be executed, it is added to the task queue. When the JavaScript engine is finished executing code, the event loop picks the next task in the queue and executes it. The *event loop* is a process inside of the JavaScript engine that monitors code execution and manages the task queue. Keep in mind that as a queue, task execution runs from the first task in the queue to the last.
15+
Because JavaScript engines can only execute one piece of code at a time, it's necessary to keep track of code that is meant to run. That code is kept in a *job queue*. Whenever a piece of code is ready to be executed, it is added to the job queue. When the JavaScript engine is finished executing code, the event loop picks the next job in the queue and executes it. The *event loop* is a process inside of the JavaScript engine that monitors code execution and manages the job queue. Keep in mind that as a queue, job execution runs from the first job in the queue to the last.
1616

1717
### Events
1818

19-
When a user clicks a button or presses key on the keyboard, an *event* is triggered (such as `onclick`). That event may be used to respond to the interaction by adding a new task to the back of the task queue. This is the most basic form of asynchronous programming JavaScript has: the event handler code doesn't execute until the event fires, and when it does execute, it has the appropriate context. For example:
19+
When a user clicks a button or presses key on the keyboard, an *event* is triggered (such as `onclick`). That event may be used to respond to the interaction by adding a new job to the back of the job queue. This is the most basic form of asynchronous programming JavaScript has: the event handler code doesn't execute until the event fires, and when it does execute, it has the appropriate context. For example:
2020

2121
```js
2222
var button = document.getElementById("my-btn");
@@ -25,7 +25,7 @@ button.onclick = function(event) {
2525
};
2626
```
2727

28-
In this code, `console.log("Clicked")` will not be executed until `button` is clicked. When `button` is clicked, the function assigned to `onclick` is added to the back of the task queue and will be executed when all other tasks ahead of it are complete.
28+
In this code, `console.log("Clicked")` will not be executed until `button` is clicked. When `button` is clicked, the function assigned to `onclick` is added to the back of the job queue and will be executed when all other jobs ahead of it are complete.
2929

3030
Events work well for simple interactions such as this, but chaining multiple separate asynchronous calls together becomes more complicated because you must keep track of the event target (`button` in the previous example) for each event. Additionally, you need to ensure all appropriate event handlers are added before the first instance of an event occurs. For instance, if `button` in the previous example was clicked before `onclick` is assigned, then nothing will happen.
3131

@@ -48,7 +48,7 @@ console.log("Hi!");
4848

4949
This example uses the traditional Node.js style of error-first callback. The `readFile()` function is intended to read from a file on disk (specified as the first argument) and then execute the callback (the second argument) when complete. If there's an error, the `err` argument of the callback is an error object; otherwise, the `contents` argument contains the file contents as a string.
5050

51-
Using the callback pattern, `readFile()` begins executing immediately and pauses when it begins reading from the disk. That means `console.log("Hi!")` is output immediately after `readFile()` is called (before `console.log(contents)`). When `readFile()` has finished, it adds a new task to the end of the task queue with the callback function and its arguments. That task is then executed upon completion of all other tasks ahead of it.
51+
Using the callback pattern, `readFile()` begins executing immediately and pauses when it begins reading from the disk. That means `console.log("Hi!")` is output immediately after `readFile()` is called (before `console.log(contents)`). When `readFile()` has finished, it adds a new job to the end of the job queue with the callback function and its arguments. That job is then executed upon completion of all other jobs ahead of it.
5252

5353
The callback pattern is more flexible than events because it is easier to chain multiple calls together. For example:
5454

@@ -68,19 +68,220 @@ readFile("example.txt", function(err, contents) {
6868
});
6969
```
7070

71-
In this code, a successful call to `readFile()` results in another asynchronous call, this time to `writeFile()`. Note that the same basic pattern of checking `err` is present in both functions. When `readFile()` is complete, it adds a task to the task queue that results in `writeFile()` being called (assuming no errors). Then, `writeFile()` adds a task to the task queue when it is complete.
71+
In this code, a successful call to `readFile()` results in another asynchronous call, this time to `writeFile()`. Note that the same basic pattern of checking `err` is present in both functions. When `readFile()` is complete, it adds a job to the job queue that results in `writeFile()` being called (assuming no errors). Then, `writeFile()` adds a job to the job queue when it is complete.
7272

73-
While this work fairly well, you can quickly get into a pattern that has come to be known as *callback hell*. Callback hell occurs when you nest too many callbacks, making the code harder to understand and read. Here's an example:
73+
While this works fairly well, you can quickly get into a pattern that has come to be known as *callback hell*. Callback hell occurs when you nest too many callbacks:
7474

75-
TODO
75+
```js
76+
method1(function(err, result) {
77+
78+
if (err) {
79+
throw err;
80+
}
81+
82+
method2(function(err, result) {
83+
84+
if (err) {
85+
throw err;
86+
}
87+
88+
method3(function(err, result) {
89+
90+
if (err) {
91+
throw err;
92+
}
93+
94+
method4(function(err, result) {
95+
96+
if (err) {
97+
throw err;
98+
}
7699

77-
## Task Scheduling
100+
method5(result);
101+
});
78102

79-
If you've ever used `setTimeout()` in a browser or Node.js, then you're already familiar with the concept of *task scheduling*. The `setTimeout()` function specifies that some code should be added to the task queue after a specified amount of time. For example:
103+
});
104+
105+
});
106+
107+
});
108+
```
109+
110+
Nesting multiple method calls, as in this example, creates a tangled web of code that is hard to understand and debug.
111+
112+
Callbacks also present problems when you want to accomplish more complex functionality. What if you'd like two asynchronous operations to run in parallel and be notified when they both are complete? What if you'd like to kick off two asynchronous operations but only take the first one to complete? In these cases, you end needing to keep track of multiple callbacks and cleanup operations. This is precisely where promises greatly improve the situation.
113+
114+
## Promise Basics
115+
116+
A promise is a placeholder for the result of an asynchronous operation. Instead of subscribing to an event or passing a callback to a function, the function can return a promise, such as:
80117

81118
```js
82-
// add this function to the task queue after 500ms have passed
119+
// readFile promises to complete at some point in the future
120+
var promise = readFile("example.txt");
121+
```
122+
123+
In this code, `readFile()` doesn't actually start reading the file immediately (that will happen later). It returns a promise object that represents the asynchronous operation so you can work with it later.
124+
125+
### Lifecycle
126+
127+
Each promise goes through a short lifecycle. It starts in the *pending* state, which is an indicator that the asynchronous operation has not yet completed. The promise in the last example is in the pending state as soon as it is returned from `readFile()`. Once the asynchronous operation completes, the promise is considered *settled* and enters one of two possible states:
128+
129+
1. *Fulfilled* - the promise's asynchronous operation has completed successfully
130+
1. *Rejected* - the promise's asynchronous operation did not complete successfully (either due to error or some other cause)
131+
132+
You can't determine which state the promise is in programmatically, but you can take a specific action when a promise changes state by using the `then()` method.
133+
134+
I> There is an internal `[[PromiseState]]` property that is set to `"pending"`, `"fulfilled"`, or `"rejected"` to reflect the promise's state.
135+
136+
The `then()` method is present on all promises and takes two arguments (any object that implements `then()` is called a *thenable*). The first argument is a function to call when the promise is fulfilled. Any additional data related to the asynchronous operation is passed into this fulfillment function. The second argument is a function to call when the promise is rejected. Similar to the fulfillment function, the rejection function is passed any additional data related to the rejection.
137+
138+
Both arguments are optional, so you can listen for any combination of fulfillment and rejection. For example:
139+
140+
```js
141+
var promise = readFile("example.txt");
142+
143+
// listen for both fulfillment and rejection
144+
promise.then(function(contents) {
145+
// fulfillment
146+
console.log(contents);
147+
}, function(err) {
148+
// rejection
149+
console.error(err.message);
150+
});
151+
152+
// listen for just fulfillment - errors are not reported
153+
promise.then(function(contents) {
154+
// fulfillment
155+
console.log(contents);
156+
});
157+
158+
// listen for just rejection - success is not reported
159+
promise.then(null, function(err) {
160+
// rejection
161+
console.error(err.message);
162+
});
163+
```
164+
165+
There is also a `catch()` method that behaves the same as `then()` when only a rejection handler is passed. For example:
166+
167+
```js
168+
promise.catch(function(err) {
169+
// rejection
170+
console.error(err.message);
171+
});
172+
173+
// is the same as:
174+
175+
promise.then(null, function(err) {
176+
// rejection
177+
console.error(err.message);
178+
});
179+
```
180+
181+
The intent is to use a combination of `then()` and `catch()` to properly handle the result of asynchronous operations. The benefit of this over both events and callbacks is that it's completely clear whether the operation succeeded or failed. (Events tend not to fire when there's an error and in callbacks you must always remember to check the error argument.)
182+
183+
### Creating Promises
184+
185+
New promises are created through the `Promise` constructor. This constructor accepts a single argument, which is a function (called the *executor*) containing the code to execute when the promise is added to the job queue. The executor is passed two functions as arguments, `resolve()` and `reject()`. The `resolve()` function is called when the executor has finished successfully in order to signal that the promise is ready to be resolved while the `reject()` function indicates that the executor has failed. Here's an example using a promise in Node.js to implement the `readFile()` function from earlier in this chapter:
186+
187+
```js
188+
// Node.js example
189+
190+
var fs = require("fs");
191+
192+
function readFile(filename) {
193+
return new Promise(function(resolve, reject) {
194+
195+
// trigger the asynchronous operation
196+
fs.readFile(filename, { encoding: "utf8" }, function(err, contents) {
197+
198+
// check for errors
199+
if (err) {
200+
reject(err);
201+
return;
202+
}
203+
204+
// the read succeeded
205+
resolve(contents);
206+
207+
});
208+
});
209+
}
210+
211+
var promise = readFile("example.txt");
212+
213+
// listen for both fulfillment and rejection
214+
promise.then(function(contents) {
215+
// fulfillment
216+
console.log(contents);
217+
}, function(err) {
218+
// rejection
219+
console.error(err.message);
220+
});
221+
222+
```
223+
224+
In this example, the native Node.js `fs.readFile()` asynchronous call is wrapped in a promise. The executor either passes the error object to `reject()` or the file contents to `resolve()`.
225+
226+
Keep in mind that the executor doesn't run immediately when `readFile()` is called. Instead, it is added as a job to the job queue. This is called *job scheduling*, and if you've ever used `setTimeout()` or `setInterval()`, then you're already familiar with it. The idea is that a new job is added to the job queue so as to say, "don't execute this right now, but execute later." In the case of `setTimeout()` and `setInterval()`, you're specifying a delay before the job is added to the queue:
227+
228+
```js
229+
// add this function to the job queue after 500ms have passed
83230
setTimeout(function() {
84-
console.log("Hi!");
231+
console.log("Timeout");
85232
}, 500)
233+
234+
console.log("Hi!");
235+
```
236+
237+
In this example, the code schedules a job to be added to the job queue after 500ms. That results in the following output:
238+
239+
```
240+
Hi!
241+
Timeout
86242
```
243+
244+
You can tell from the output that the function passed to `setTimeout()` was executed after `console.log("Hi!")`. Promises work in a similar way.
245+
246+
The promise executor is added to the job queue immediately, meaning it will execute only after all previous jobs are complete. For example:
247+
248+
```js
249+
var promise = new Promise(function(resolve, reject) {
250+
console.log("Promise");
251+
resolve();
252+
});
253+
254+
console.log("Hi!");
255+
```
256+
257+
The output for this example is:
258+
259+
```
260+
Hi!
261+
Promise
262+
```
263+
264+
The takeaway is that the executor doesn't run until sometime after the current job has finished executing. The same is true for the functions passed to `then()` and `catch()`, as these will also be added to the job queue, but only after the executor job. Here's an example:
265+
266+
```js
267+
var promise = new Promise(function(resolve, reject) {
268+
console.log("Promise");
269+
resolve();
270+
});
271+
272+
promise.then(function() {
273+
console.log("Resolved.");
274+
});
275+
276+
console.log("Hi!");
277+
```
278+
279+
The output for this example is:
280+
281+
```
282+
Hi!
283+
Promise
284+
Resolved
285+
```
286+
287+
The fulfillment and rejection handlers are always added to the end of the job queue after the executor has completed.

0 commit comments

Comments
 (0)