Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 8647eb5

Browse files
committed
Merge branch 'master' of github.com:javascript-tutorial/en.javascript.info into sync-035c5267
2 parents a94687a + 035c526 commit 8647eb5

File tree

2 files changed

+192
-0
lines changed

2 files changed

+192
-0
lines changed
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
2+
The root of the problem is that `Promise.all` immediately rejects when one of its promises rejects, but it do nothing to cancel the other promises.
3+
4+
In our case, the second query fails, so `Promise.all` rejects, and the `try...catch` block catches this error.Meanwhile, other promises are *not affected* - they independently continue their execution. In our case, the third query throws an error of its own after a bit of time. And that error is never caught, we can see it in the console.
5+
6+
The problem is especially dangerous in server-side environments, such as Node.js, when an uncaught error may cause the process to crash.
7+
8+
How to fix it?
9+
10+
An ideal solution would be to cancel all unfinished queries when one of them fails. This way we avoid any potential errors.
11+
12+
However, the bad news is that service calls (such as `database.query`) are often implemented by a 3rd-party library which doesn't support cancellation. Then there's no way to cancel a call.
13+
14+
As an alternative, we can write our own wrapper function around `Promise.all` which adds a custom `then/catch` handler to each promise to track them: results are gathered and, if an error occurs, all subsequent promises are ignored.
15+
16+
```js
17+
function customPromiseAll(promises) {
18+
return new Promise((resolve, reject) => {
19+
const results = [];
20+
let resultsCount = 0;
21+
let hasError = false; // we'll set it to true upon first error
22+
23+
promises.forEach((promise, index) => {
24+
promise
25+
.then(result => {
26+
if (hasError) return; // ignore the promise if already errored
27+
results[index] = result;
28+
resultsCount++;
29+
if (resultsCount === promises.length) {
30+
resolve(results); // when all results are ready - successs
31+
}
32+
})
33+
.catch(error => {
34+
if (hasError) return; // ignore the promise if already errored
35+
hasError = true; // wops, error!
36+
reject(error); // fail with rejection
37+
});
38+
});
39+
});
40+
}
41+
```
42+
43+
This approach has an issue of its own - it's often undesirable to `disconnect()` when queries are still in the process.
44+
45+
It may be important that all queries complete, especially if some of them make important updates.
46+
47+
So we should wait until all promises are settled before going further with the execution and eventually disconnecting.
48+
49+
Here's another implementation. It behaves similar to `Promise.all` - also resolves with the first error, but waits until all promises are settled.
50+
51+
```js
52+
function customPromiseAllWait(promises) {
53+
return new Promise((resolve, reject) => {
54+
const results = new Array(promises.length);
55+
let settledCount = 0;
56+
let firstError = null;
57+
58+
promises.forEach((promise, index) => {
59+
Promise.resolve(promise)
60+
.then(result => {
61+
results[index] = result;
62+
})
63+
.catch(error => {
64+
if (firstError === null) {
65+
firstError = error;
66+
}
67+
})
68+
.finally(() => {
69+
settledCount++;
70+
if (settledCount === promises.length) {
71+
if (firstError !== null) {
72+
reject(firstError);
73+
} else {
74+
resolve(results);
75+
}
76+
}
77+
});
78+
});
79+
});
80+
}
81+
```
82+
83+
Now `await customPromiseAllWait(...)` will stall the execution until all queries are processed.
84+
85+
This is a more reliable approach, as it guarantees a predictable execution flow.
86+
87+
Lastly, if we'd like to process all errors, we can use either use `Promise.allSettled` or write a wrapper around it to gathers all errors in a single [AggregateError](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AggregateError) object and rejects with it.
88+
89+
```js
90+
// wait for all promises to settle
91+
// return results if no errors
92+
// throw AggregateError with all errors if any
93+
function allOrAggregateError(promises) {
94+
return Promise.allSettled(promises).then(results => {
95+
const errors = [];
96+
const values = [];
97+
98+
results.forEach((res, i) => {
99+
if (res.status === 'fulfilled') {
100+
values[i] = res.value;
101+
} else {
102+
errors.push(res.reason);
103+
}
104+
});
105+
106+
if (errors.length > 0) {
107+
throw new AggregateError(errors, 'One or more promises failed');
108+
}
109+
110+
return values;
111+
});
112+
}
113+
```
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
2+
# Dangerous Promise.all
3+
4+
`Promise.all` is a great way to parallelize multiple operations. It's especially useful when we need to make parallel requests to multiple services.
5+
6+
However, there's a hidden danger. We'll see an example in this task and explore how to avoid it.
7+
8+
Let's say we have a connection to a remote service, such as a database.
9+
10+
There're two functions: `connect()` and `disconnect()`.
11+
12+
When connected, we can send requests using `database.query(...)` - an async function which usually returns the result but also may throw an error.
13+
14+
Here's a simple implementation:
15+
16+
```js
17+
let database;
18+
19+
function connect() {
20+
database = {
21+
async query(isOk) {
22+
if (!isOk) throw new Error('Query failed');
23+
}
24+
};
25+
}
26+
27+
function disconnect() {
28+
database = null;
29+
}
30+
31+
// intended usage:
32+
// connect()
33+
// ...
34+
// database.query(true) to emulate a successful call
35+
// database.query(false) to emulate a failed call
36+
// ...
37+
// disconnect()
38+
```
39+
40+
Now here's the problem.
41+
42+
We wrote the code to connect and send 3 queries in parallel (all of them take different time, e.g. 100, 200 and 300ms), then disconnect:
43+
44+
```js
45+
// Helper function to call async function `fn` after `ms` milliseconds
46+
function delay(fn, ms) {
47+
return new Promise((resolve, reject) => {
48+
setTimeout(() => fn().then(resolve, reject), ms);
49+
});
50+
}
51+
52+
async function run() {
53+
connect();
54+
55+
try {
56+
await Promise.all([
57+
// these 3 parallel jobs take different time: 100, 200 and 300 ms
58+
// we use the `delay` helper to achieve this effect
59+
*!*
60+
delay(() => database.query(true), 100),
61+
delay(() => database.query(false), 200),
62+
delay(() => database.query(false), 300)
63+
*/!*
64+
]);
65+
} catch(error) {
66+
console.log('Error handled (or was it?)');
67+
}
68+
69+
disconnect();
70+
}
71+
72+
run();
73+
```
74+
75+
Two of these queries happen to be unsuccessful, but we're smart enough to wrap the `Promise.all` call into a `try..catch` block.
76+
77+
However, this doesn't help! This script actually leads to an uncaught error in console!
78+
79+
Why? How to avoid it?

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /