Skip to content

Commit fdb99a6

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

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)