-
Notifications
You must be signed in to change notification settings - Fork 22.4k
/
index.md
555 lines (425 loc) · 24.2 KB
/
index.md
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
---
title: Using promises
slug: Web/JavaScript/Guide/Using_promises
page-type: guide
---
{{jsSidebar("JavaScript Guide")}}{{PreviousNext("Web/JavaScript/Guide/Using_classes", "Web/JavaScript/Guide/Typed_arrays")}}
A {{jsxref("Promise")}} is an object representing the eventual completion or failure of an asynchronous operation. Since most people are consumers of already-created promises, this guide will explain consumption of returned promises before explaining how to create them.
Essentially, a promise is a returned object to which you attach callbacks, instead of passing callbacks into a function. Imagine a function, `createAudioFileAsync()`, which asynchronously generates a sound file given a configuration record and two callback functions: one called if the audio file is successfully created, and the other called if an error occurs.
Here's some code that uses `createAudioFileAsync()`:
```js
function successCallback(result) {
console.log(`Audio file ready at URL: ${result}`);
}
function failureCallback(error) {
console.error(`Error generating audio file: ${error}`);
}
createAudioFileAsync(audioSettings, successCallback, failureCallback);
```
If `createAudioFileAsync()` were rewritten to return a promise, you would attach your callbacks to it instead:
```js
createAudioFileAsync(audioSettings).then(successCallback, failureCallback);
```
This convention has several advantages. We will explore each one.
## Chaining
A common need is to execute two or more asynchronous operations back to back, where each subsequent operation starts when the previous operation succeeds, with the result from the previous step. In the old days, doing several asynchronous operations in a row would lead to the classic [callback hell](http://callbackhell.com/):
```js-nolint
doSomething(function (result) {
doSomethingElse(result, function (newResult) {
doThirdThing(newResult, function (finalResult) {
console.log(`Got the final result: ${finalResult}`);
}, failureCallback);
}, failureCallback);
}, failureCallback);
```
With promises, we accomplish this by creating a promise chain. The API design of promises makes this great, because callbacks are attached to the returned promise object, instead of being passed into a function.
Here's the magic: the `then()` function returns a **new promise**, different from the original:
```js
const promise = doSomething();
const promise2 = promise.then(successCallback, failureCallback);
```
This second promise (`promise2`) represents the completion not just of `doSomething()`, but also of the `successCallback` or `failureCallback` you passed in — which can be other asynchronous functions returning a promise. When that's the case, any callbacks added to `promise2` get queued behind the promise returned by either `successCallback` or `failureCallback`.
> [!NOTE]
> If you want a working example to play with, you can use the following template to create any function returning a promise:
>
> ```js
> function doSomething() {
> return new Promise((resolve) => {
> setTimeout(() => {
> // Other things to do before completion of the promise
> console.log("Did something");
> // The fulfillment value of the promise
> resolve("https://example.com/");
> }, 200);
> });
> }
> ```
>
> The implementation is discussed in the [Creating a Promise around an old callback API](#creating_a_promise_around_an_old_callback_api) section below.
With this pattern, you can create longer chains of processing, where each promise represents the completion of one asynchronous step in the chain. In addition, the arguments to `then` are optional, and `catch(failureCallback)` is short for `then(null, failureCallback)` — so if your error handling code is the same for all steps, you can attach it to the end of the chain:
```js
doSomething()
.then(function (result) {
return doSomethingElse(result);
})
.then(function (newResult) {
return doThirdThing(newResult);
})
.then(function (finalResult) {
console.log(`Got the final result: ${finalResult}`);
})
.catch(failureCallback);
```
You might see this expressed with [arrow functions](/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions) instead:
```js
doSomething()
.then((result) => doSomethingElse(result))
.then((newResult) => doThirdThing(newResult))
.then((finalResult) => {
console.log(`Got the final result: ${finalResult}`);
})
.catch(failureCallback);
```
> [!NOTE]
> Arrow function expressions can have an [implicit return](/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions#function_body); so, `() => x` is short for `() => { return x; }`.
`doSomethingElse` and `doThirdThing` can return any value — if they return promises, that promise is first waited until it settles, and the next callback receives the fulfillment value, not the promise itself. It is important to always return promises from `then` callbacks, even if the promise always resolves to `undefined`. If the previous handler started a promise but did not return it, there's no way to track its settlement anymore, and the promise is said to be "floating".
```js example-bad
doSomething()
.then((url) => {
// Missing `return` keyword in front of fetch(url).
fetch(url);
})
.then((result) => {
// result is undefined, because nothing is returned from the previous
// handler. There's no way to know the return value of the fetch()
// call anymore, or whether it succeeded at all.
});
```
By returning the result of the `fetch` call (which is a promise), we can both track its completion and receive its value when it completes.
```js example-good
doSomething()
.then((url) => {
// `return` keyword added
return fetch(url);
})
.then((result) => {
// result is a Response object
});
```
Floating promises could be worse if you have race conditions — if the promise from the last handler is not returned, the next `then` handler will be called early, and any value it reads may be incomplete.
```js example-bad
const listOfIngredients = [];
doSomething()
.then((url) => {
// Missing `return` keyword in front of fetch(url).
fetch(url)
.then((res) => res.json())
.then((data) => {
listOfIngredients.push(data);
});
})
.then(() => {
console.log(listOfIngredients);
// listOfIngredients will always be [], because the fetch request hasn't completed yet.
});
```
Therefore, as a rule of thumb, whenever your operation encounters a promise, return it and defer its handling to the next `then` handler.
```js example-good
const listOfIngredients = [];
doSomething()
.then((url) => {
// `return` keyword now included in front of fetch call.
return fetch(url)
.then((res) => res.json())
.then((data) => {
listOfIngredients.push(data);
});
})
.then(() => {
console.log(listOfIngredients);
// listOfIngredients will now contain data from fetch call.
});
```
Even better, you can flatten the nested chain into a single chain, which is simpler and makes error handling easier. The details are discussed in the [Nesting](#nesting) section below.
```js
doSomething()
.then((url) => fetch(url))
.then((res) => res.json())
.then((data) => {
listOfIngredients.push(data);
})
.then(() => {
console.log(listOfIngredients);
});
```
Using [`async`/`await`](/en-US/docs/Web/JavaScript/Reference/Statements/async_function) can help you write code that's more intuitive and resembles synchronous code. Below is the same example using `async`/`await`:
```js
async function logIngredients() {
const url = await doSomething();
const res = await fetch(url);
const data = await res.json();
listOfIngredients.push(data);
console.log(listOfIngredients);
}
```
Note how the code looks exactly like synchronous code, except for the `await` keywords in front of promises. One of the only tradeoffs is that it may be easy to forget the [`await`](/en-US/docs/Web/JavaScript/Reference/Statements/async_function) keyword, which can only be fixed when there's a type mismatch (e.g. trying to use a promise as a value).
`async`/`await` builds on promises — for example, `doSomething()` is the same function as before, so there's minimal refactoring needed to change from promises to `async`/`await`. You can read more about the `async`/`await` syntax in the [async functions](/en-US/docs/Web/JavaScript/Reference/Statements/async_function) and [`await`](/en-US/docs/Web/JavaScript/Reference/Operators/await) references.
> **Note:** `async`/`await` has the same concurrency semantics as normal promise chains. `await` within one async function does not stop the entire program, only the parts that depend on its value, so other async jobs can still run while the `await` is pending.
## Error handling
You might recall seeing `failureCallback` three times in the pyramid of doom earlier, compared to only once at the end of the promise chain:
```js
doSomething()
.then((result) => doSomethingElse(result))
.then((newResult) => doThirdThing(newResult))
.then((finalResult) => console.log(`Got the final result: ${finalResult}`))
.catch(failureCallback);
```
If there's an exception, the browser will look down the chain for `.catch()` handlers or `onRejected`. This is very much modeled after how synchronous code works:
```js
try {
const result = syncDoSomething();
const newResult = syncDoSomethingElse(result);
const finalResult = syncDoThirdThing(newResult);
console.log(`Got the final result: ${finalResult}`);
} catch (error) {
failureCallback(error);
}
```
This symmetry with asynchronous code culminates in the `async`/`await` syntax:
```js
async function foo() {
try {
const result = await doSomething();
const newResult = await doSomethingElse(result);
const finalResult = await doThirdThing(newResult);
console.log(`Got the final result: ${finalResult}`);
} catch (error) {
failureCallback(error);
}
}
```
Promises solve a fundamental flaw with the callback pyramid of doom, by catching all errors, even thrown exceptions and programming errors. This is essential for functional composition of asynchronous operations. All errors are now handled by the [`catch()`](/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/catch) method at the end of the chain, and you should almost never need to use `try`/`catch` without using `async`/`await`.
### Nesting
In the examples above involving `listOfIngredients`, the first one has one promise chain nested in the return value of another `then()` handler, while the second one uses an entirely flat chain. Simple promise chains are best kept flat without nesting, as nesting can be a result of careless composition.
Nesting is a control structure to limit the scope of `catch` statements. Specifically, a nested `catch` only catches failures in its scope and below, not errors higher up in the chain outside the nested scope. When used correctly, this gives greater precision in error recovery:
```js
doSomethingCritical()
.then((result) =>
doSomethingOptional(result)
.then((optionalResult) => doSomethingExtraNice(optionalResult))
.catch((e) => {}),
) // Ignore if optional stuff fails; proceed.
.then(() => moreCriticalStuff())
.catch((e) => console.error(`Critical failure: ${e.message}`));
```
Note that the optional steps here are nested — with the nesting caused not by the indentation, but by the placement of the outer `(` and `)` parentheses around the steps.
The inner error-silencing `catch` handler only catches failures from `doSomethingOptional()` and `doSomethingExtraNice()`, after which the code resumes with `moreCriticalStuff()`. Importantly, if `doSomethingCritical()` fails, its error is caught by the final (outer) `catch` only, and does not get swallowed by the inner `catch` handler.
In `async`/`await`, this code looks like:
```js
async function main() {
try {
const result = await doSomethingCritical();
try {
const optionalResult = await doSomethingOptional(result);
await doSomethingExtraNice(optionalResult);
} catch (e) {
// Ignore failures in optional steps and proceed.
}
await moreCriticalStuff();
} catch (e) {
console.error(`Critical failure: ${e.message}`);
}
}
```
> [!NOTE]
> If you don't have sophisticated error handling, you very likely don't need nested `then` handlers. Instead, use a flat chain and put the error handling logic at the end.
### Chaining after a catch
It's possible to chain _after_ a failure, i.e. a `catch`, which is useful to accomplish new actions even after an action failed in the chain. Read the following example:
```js
doSomething()
.then(() => {
throw new Error("Something failed");
console.log("Do this");
})
.catch(() => {
console.error("Do that");
})
.then(() => {
console.log("Do this, no matter what happened before");
});
```
This will output the following text:
```plain
Initial
Do that
Do this, no matter what happened before
```
> [!NOTE]
> The text "Do this" is not displayed because the "Something failed" error caused a rejection.
In `async`/`await`, this code looks like:
```js
async function main() {
try {
await doSomething();
throw new Error("Something failed");
console.log("Do this");
} catch (e) {
console.error("Do that");
}
console.log("Do this, no matter what happened before");
}
```
### Promise rejection events
If a promise rejection event is not handled by any handler, it bubbles to the top of the call stack, and the host needs to surface it. On the web, whenever a promise is rejected, one of two events is sent to the global scope (generally, this is either the [`window`](/en-US/docs/Web/API/Window) or, if being used in a web worker, it's the [`Worker`](/en-US/docs/Web/API/Worker) or other worker-based interface). The two events are:
- [`unhandledrejection`](/en-US/docs/Web/API/Window/unhandledrejection_event)
- : Sent when a promise is rejected but there is no rejection handler available.
- [`rejectionhandled`](/en-US/docs/Web/API/Window/rejectionhandled_event)
- : Sent when a handler is attached to a rejected promise that has already caused an `unhandledrejection` event.
In both cases, the event (of type [`PromiseRejectionEvent`](/en-US/docs/Web/API/PromiseRejectionEvent)) has as members a [`promise`](/en-US/docs/Web/API/PromiseRejectionEvent/promise) property indicating the promise that was rejected, and a [`reason`](/en-US/docs/Web/API/PromiseRejectionEvent/reason) property that provides the reason given for the promise to be rejected.
These make it possible to offer fallback error handling for promises, as well as to help debug issues with your promise management. These handlers are global per context, so all errors will go to the same event handlers, regardless of source.
In [Node.js](/en-US/docs/Glossary/Node.js), handling promise rejection is slightly different. You capture unhandled rejections by adding a handler for the Node.js `unhandledRejection` event (notice the difference in capitalization of the name), like this:
```js
process.on("unhandledRejection", (reason, promise) => {
// Add code here to examine the "promise" and "reason" values
});
```
For Node.js, to prevent the error from being logged to the console (the default action that would otherwise occur), adding that `process.on()` listener is all that's necessary; there's no need for an equivalent of the browser runtime's [`preventDefault()`](/en-US/docs/Web/API/Event/preventDefault) method.
However, if you add that `process.on` listener but don't also have code within it to handle rejected promises, they will just be dropped on the floor and silently ignored. So ideally, you should add code within that listener to examine each rejected promise and make sure it was not caused by an actual code bug.
## Composition
There are four [composition tools](/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise#promise_concurrency) for running asynchronous operations concurrently: {{jsxref("Promise.all()")}}, {{jsxref("Promise.allSettled()")}}, {{jsxref("Promise.any()")}}, and {{jsxref("Promise.race()")}}.
We can start operations at the same time and wait for them all to finish like this:
```js
Promise.all([func1(), func2(), func3()]).then(([result1, result2, result3]) => {
// use result1, result2 and result3
});
```
If one of the promises in the array rejects, `Promise.all()` immediately rejects the returned promise and aborts the other operations. This may cause unexpected state or behavior. {{jsxref("Promise.allSettled()")}} is another composition tool that ensures all operations are complete before resolving.
These methods all run promises concurrently — a sequence of promises are started simultaneously and do not wait for each other. Sequential composition is possible using some clever JavaScript:
```js
[func1, func2, func3]
.reduce((p, f) => p.then(f), Promise.resolve())
.then((result3) => {
/* use result3 */
});
```
In this example, we [reduce](/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce) an array of asynchronous functions down to a promise chain. The code above is equivalent to:
```js
Promise.resolve()
.then(func1)
.then(func2)
.then(func3)
.then((result3) => {
/* use result3 */
});
```
This can be made into a reusable compose function, which is common in functional programming:
```js
const applyAsync = (acc, val) => acc.then(val);
const composeAsync =
(...funcs) =>
(x) =>
funcs.reduce(applyAsync, Promise.resolve(x));
```
The `composeAsync()` function accepts any number of functions as arguments and returns a new function that accepts an initial value to be passed through the composition pipeline:
```js
const transformData = composeAsync(func1, func2, func3);
const result3 = transformData(data);
```
Sequential composition can also be done more succinctly with async/await:
```js
let result;
for (const f of [func1, func2, func3]) {
result = await f(result);
}
/* use last result (i.e. result3) */
```
However, before you compose promises sequentially, consider if it's really necessary — it's always better to run promises concurrently so that they don't unnecessarily block each other unless one promise's execution depends on another's result.
## Cancellation
`Promise` itself has no first-class protocol for cancellation, but you may be able to directly cancel the underlying asynchronous operation, typically using [`AbortController`](/en-US/docs/Web/API/AbortController).
## Creating a Promise around an old callback API
A {{jsxref("Promise")}} can be created from scratch using its [constructor](/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/Promise). This should be needed only to wrap old APIs.
In an ideal world, all asynchronous functions would already return promises. Unfortunately, some APIs still expect success and/or failure callbacks to be passed in the old way. The most obvious example is the [`setTimeout()`](/en-US/docs/Web/API/setTimeout) function:
```js
setTimeout(() => saySomething("10 seconds passed"), 10 * 1000);
```
Mixing old-style callbacks and promises is problematic. If `saySomething()` fails or contains a programming error, nothing catches it. This is intrinsic to the design of `setTimeout`.
Luckily we can wrap `setTimeout` in a promise. The best practice is to wrap the callback-accepting functions at the lowest possible level, and then never call them directly again:
```js
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
wait(10 * 1000)
.then(() => saySomething("10 seconds"))
.catch(failureCallback);
```
The promise constructor takes an executor function that lets us resolve or reject a promise manually. Since `setTimeout()` doesn't really fail, we left out reject in this case. For more information on how the executor function works, see the [`Promise()`](/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/Promise) reference.
## Timing
Lastly, we will look into the more technical details, about when the registered callbacks get called.
### Guarantees
In the callback-based API, when and how the callback gets called depends on the API implementor. For example, the callback may be called synchronously or asynchronously:
```js example-bad
function doSomething(callback) {
if (Math.random() > 0.5) {
callback();
} else {
setTimeout(() => callback(), 1000);
}
}
```
The above design is strongly discouraged because it leads to the so-called "state of Zalgo". In the context of designing asynchronous APIs, this means a callback is called synchronously in some cases but asynchronously in other cases, creating ambiguity for the caller. For further background, see the article [Designing APIs for Asynchrony](https://blog.izs.me/2013/08/designing-apis-for-asynchrony/), where the term was first formally presented. This API design makes side effects hard to analyze:
```js
let value = 1;
doSomething(() => {
value = 2;
});
console.log(value); // 1 or 2?
```
On the other hand, promises are a form of [inversion of control](https://en.wikipedia.org/wiki/Inversion_of_control) — the API implementor does not control when the callback gets called. Instead, the job of maintaining the callback queue and deciding when to call the callbacks is delegated to the promise implementation, and both the API user and API developer automatically gets strong semantic guarantees, including:
- Callbacks added with [`then()`](/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then) will never be invoked before the [completion of the current run](/en-US/docs/Web/JavaScript/Event_loop#run-to-completion) of the JavaScript event loop.
- These callbacks will be invoked even if they were added _after_ the success or failure of the asynchronous operation that the promise represents.
- Multiple callbacks may be added by calling [`then()`](/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then) several times. They will be invoked one after another, in the order in which they were inserted.
To avoid surprises, functions passed to [`then()`](/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then) will never be called synchronously, even with an already-resolved promise:
```js
Promise.resolve().then(() => console.log(2));
console.log(1);
// Logs: 1, 2
```
Instead of running immediately, the passed-in function is put on a microtask queue, which means it runs later (only after the function which created it exits, and when the JavaScript execution stack is empty), just before control is returned to the event loop; i.e. pretty soon:
```js
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
wait(0).then(() => console.log(4));
Promise.resolve()
.then(() => console.log(2))
.then(() => console.log(3));
console.log(1); // 1, 2, 3, 4
```
### Task queues vs. microtasks
Promise callbacks are handled as a [microtask](/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide) whereas [`setTimeout()`](/en-US/docs/Web/API/setTimeout) callbacks are handled as task queues.
```js
const promise = new Promise((resolve, reject) => {
console.log("Promise callback");
resolve();
}).then((result) => {
console.log("Promise callback (.then)");
});
setTimeout(() => {
console.log("event-loop cycle: Promise (fulfilled)", promise);
}, 0);
console.log("Promise (pending)", promise);
```
The code above will output:
```plain
Promise callback
Promise (pending) Promise {<pending>}
Promise callback (.then)
event-loop cycle: Promise (fulfilled) Promise {<fulfilled>}
```
For more details, refer to [Tasks vs. microtasks](/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide/In_depth#tasks_vs._microtasks).
### When promises and tasks collide
If you run into situations in which you have promises and tasks (such as events or callbacks) which are firing in unpredictable orders, it's possible you may benefit from using a microtask to check status or balance out your promises when promises are created conditionally.
If you think microtasks may help solve this problem, see the [microtask guide](/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide) to learn more about how to use [`queueMicrotask()`](/en-US/docs/Web/API/queueMicrotask) to enqueue a function as a microtask.
## See also
- {{jsxref("Promise")}}
- {{jsxref("Statements/async_function", "async function")}}
- {{jsxref("Operators/await", "await")}}
- [Promises/A+ specification](https://promisesaplus.com/)
- [We have a problem with promises](https://pouchdb.com/2015/05/18/we-have-a-problem-with-promises.html) on pouchdb.com (2015)
{{PreviousNext("Web/JavaScript/Guide/Using_classes", "Web/JavaScript/Guide/Typed_arrays")}}