Skip to content

Commit 3de63df

Browse files
committed
promise.all task
1 parent 3d7abb9 commit 3de63df

File tree

2 files changed

+161
-0
lines changed

2 files changed

+161
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
2+
The problem is that `Promise.all` immediately rejects when one of its promises rejects. In our case, the second query fails, so `Promise.all` rejects, and the `try...catch` block catches this error.
3+
4+
Meanwhile, even if one of the queries fails, 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+
A natural 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. So there's usually no way to cancel a call.
13+
14+
Instead 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 one more implementation. It 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 the most reliable approach.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
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. Hopefully we'll be able to identify its cause.
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 write a simple 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+
delay(() => database.query(true), 100),
59+
delay(() => database.query(false), 200),
60+
delay(() => database.query(false), 300)
61+
]);
62+
} catch(error) {
63+
console.log('Error handled (or was it?)');
64+
}
65+
66+
disconnect();
67+
}
68+
69+
run();
70+
```
71+
72+
Two of these queries are (by chance) unsuccessful, but we're smart enough to wrap the `Promise.all` call into a `try...catch` block.
73+
74+
However, this script actually leads to an uncaught error in console!
75+
76+
Why? How to avoid it?

0 commit comments

Comments
 (0)