Skip to content

Commit 42c894d

Browse files
committed
Update Promise.all and Promise.allSettled to catch a possible abrupt completion while calling Promise.resolve
When calling these methods with an empty iterator, they will call Promise.resolve synchronously with an empty array result. If Promise.resolve results in an abrupt completion, we are supposed to catch this and reject the result promise. Previously we were just letting this abrupt completion be leaked.
1 parent c50e51b commit 42c894d

File tree

3 files changed

+121
-99
lines changed

3 files changed

+121
-99
lines changed

lib/Runtime/Library/JavascriptPromise.cpp

Lines changed: 16 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,14 @@ namespace Js
246246

247247
index++;
248248
});
249+
250+
remainingElementsWrapper->remainingElements--;
251+
if (remainingElementsWrapper->remainingElements == 0)
252+
{
253+
Assert(values != nullptr);
254+
255+
TryCallResolveOrRejectHandler(promiseCapability->GetResolve(), values, scriptContext);
256+
}
249257
}
250258
catch (const JavascriptException& err)
251259
{
@@ -255,20 +263,6 @@ namespace Js
255263
if (exception != nullptr)
256264
{
257265
TryRejectWithExceptionObject(exception, promiseCapability->GetReject(), scriptContext);
258-
259-
// We need to explicitly return here to make sure we don't resolve in case index == 0 here.
260-
// That would happen if GetIterator or IteratorValue throws an exception in the first iteration.
261-
return promiseCapability->GetPromise();
262-
}
263-
264-
remainingElementsWrapper->remainingElements--;
265-
266-
// We want this call to happen outside the try statement because if it throws, we aren't supposed to reject the promise.
267-
if (remainingElementsWrapper->remainingElements == 0)
268-
{
269-
Assert(values != nullptr);
270-
271-
TryCallResolveOrRejectHandler(promiseCapability->GetResolve(), values, scriptContext);
272266
}
273267

274268
return promiseCapability->GetPromise();
@@ -388,6 +382,14 @@ namespace Js
388382

389383
index++;
390384
});
385+
386+
remainingElementsWrapper->remainingElements--;
387+
if (remainingElementsWrapper->remainingElements == 0)
388+
{
389+
Assert(values != nullptr);
390+
391+
TryCallResolveOrRejectHandler(promiseCapability->GetResolve(), values, scriptContext);
392+
}
391393
}
392394
catch (const JavascriptException& err)
393395
{
@@ -397,20 +399,6 @@ namespace Js
397399
if (exception != nullptr)
398400
{
399401
TryRejectWithExceptionObject(exception, promiseCapability->GetReject(), scriptContext);
400-
401-
// We need to explicitly return here to make sure we don't resolve in case index == 0 here.
402-
// That would happen if GetIterator or IteratorValue throws an exception in the first iteration.
403-
return promiseCapability->GetPromise();
404-
}
405-
406-
remainingElementsWrapper->remainingElements--;
407-
408-
// We want this call to happen outside the try statement because if it throws, we aren't supposed to reject the promise.
409-
if (remainingElementsWrapper->remainingElements == 0)
410-
{
411-
Assert(values != nullptr);
412-
413-
TryCallResolveOrRejectHandler(promiseCapability->GetResolve(), values, scriptContext);
414402
}
415403

416404
return promiseCapability->GetPromise();

test/es6/ES6PromiseAsync.baseline

Lines changed: 77 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -39,37 +39,43 @@ Executing test #36 - Promise.all fulfills with the same value as the first encou
3939
Executing test #37 - Promise.all fulfills with the same value as the first encountered rejected promise (async promises)
4040
Executing test #38 - Promise.all fulfills when all promises in iterable fulfill
4141
Executing test #39 - Promise.all passes each element in the arguments to Promise.resolve
42-
Executing test #40 - Promise.resolve called with a thenable calls then on the thenable
43-
Executing test #41 - Calling promise resolve function with thenable should call thenable.then
44-
Executing test #42 - Promise.all doesn't call then for rejected promises
45-
Executing test #43 - Promise.all with iterator that returns no items
46-
Executing test #44 - Simple tampering of Promise.all promise changes resolved result value
47-
Executing test #45 - Promise.all - can't prevent remaining elements counter from reaching zero
48-
Executing test #46 - Promise from Promise.all never resolved before arguments
49-
Executing test #47 - Promise from Promise.all never resolved if rejected promise in arguments
50-
Executing test #48 - Promise executor resolves with the first call resolve function
51-
Executing test #49 - Promise executor rejects with the first call reject function
52-
Executing test #50 - Promise executor resolves/rejects with the first call to either function
53-
Executing test #51 - Promise executor rejects/resolves with the first call to either function
54-
Executing test #52 - Promise executor rejects/resolves/rejects with the first call to either function
55-
Executing test #53 - Promise executor resolves/rejects/resolves with the first call to either function
56-
Executing test #54 - Promise.prototype.finally - called for resolved promise
57-
Executing test #55 - Promise.prototype.finally - called for rejected promise
58-
Executing test #56 - Promise.prototype.finally passes through result for Reject
59-
Executing test #57 - Promise.prototype.finally passes through result for Resolve
60-
Executing test #58 - Promise.prototype.finally passes through result for Reject with not callable argument
61-
Executing test #59 - Promise.prototype.finally passes through result for Resolve with not callable argument
62-
Executing test #60 - Promise.prototype.finally throws own rejection after a resolved promise
63-
Executing test #61 - Promise.prototype.finally throws own rejection after a rejected promise
64-
Executing test #62 - Ensure Multiple then handlers on a single promise are executed in correct order
65-
Executing test #63 - Promise.allSettled settles when all promises in iterable settle
66-
Executing test #64 - Promise.allSettled settles even if all promises reject
67-
Executing test #65 - Promise.allSettled settles immediately with an empty iterator
68-
Executing test #66 - Promise.allSettled doesn't settle if some promises don't settle
69-
Executing test #67 - Promise.allSettled rejects immediately with an iterator that throws
70-
Executing test #68 - Promise.allSettled resolve and reject functions cannot be called multiple times
71-
Test #68 - Temp then handler called from Promise.allSettled
72-
Executing test #69 - Promise.allSettled passes all elements of iterable to Promise.resolve
42+
Executing test #40 - Promise.all called with empty iterator, calls Promise.resolve synchronously and passes abrupt completion to reject handler
43+
Test #40 - resolve called
44+
Test #40 - reject called: oops
45+
Executing test #41 - Promise.resolve called with a thenable calls then on the thenable
46+
Executing test #42 - Calling promise resolve function with thenable should call thenable.then
47+
Executing test #43 - Promise.all doesn't call then for rejected promises
48+
Executing test #44 - Promise.all with iterator that returns no items
49+
Executing test #45 - Simple tampering of Promise.all promise changes resolved result value
50+
Executing test #46 - Promise.all - can't prevent remaining elements counter from reaching zero
51+
Executing test #47 - Promise from Promise.all never resolved before arguments
52+
Executing test #48 - Promise from Promise.all never resolved if rejected promise in arguments
53+
Executing test #49 - Promise executor resolves with the first call resolve function
54+
Executing test #50 - Promise executor rejects with the first call reject function
55+
Executing test #51 - Promise executor resolves/rejects with the first call to either function
56+
Executing test #52 - Promise executor rejects/resolves with the first call to either function
57+
Executing test #53 - Promise executor rejects/resolves/rejects with the first call to either function
58+
Executing test #54 - Promise executor resolves/rejects/resolves with the first call to either function
59+
Executing test #55 - Promise.prototype.finally - called for resolved promise
60+
Executing test #56 - Promise.prototype.finally - called for rejected promise
61+
Executing test #57 - Promise.prototype.finally passes through result for Reject
62+
Executing test #58 - Promise.prototype.finally passes through result for Resolve
63+
Executing test #59 - Promise.prototype.finally passes through result for Reject with not callable argument
64+
Executing test #60 - Promise.prototype.finally passes through result for Resolve with not callable argument
65+
Executing test #61 - Promise.prototype.finally throws own rejection after a resolved promise
66+
Executing test #62 - Promise.prototype.finally throws own rejection after a rejected promise
67+
Executing test #63 - Ensure Multiple then handlers on a single promise are executed in correct order
68+
Executing test #64 - Promise.allSettled settles when all promises in iterable settle
69+
Executing test #65 - Promise.allSettled settles even if all promises reject
70+
Executing test #66 - Promise.allSettled settles immediately with an empty iterator
71+
Executing test #67 - Promise.allSettled doesn't settle if some promises don't settle
72+
Executing test #68 - Promise.allSettled rejects immediately with an iterator that throws
73+
Executing test #69 - Promise.allSettled resolve and reject functions cannot be called multiple times
74+
Test #69 - Temp then handler called from Promise.allSettled
75+
Executing test #70 - Promise.allSettled passes all elements of iterable to Promise.resolve
76+
Executing test #71 - Promise.allsettled called with empty iterator, calls Promise.resolve synchronously and passes abrupt completion to reject handler
77+
Test #71 - resolve called
78+
Test #71 - reject called: oops
7379

7480
Completion Results:
7581
Test #1 - Success handler called with result = basic:success
@@ -100,23 +106,23 @@ Test #32 - Catch handler #1 called with err = TypeError: Object expected
100106
Test #33 - Catch handler #1 called with err = TypeError: Function expected
101107
Test #34 - Catch handler #1 called with err = TypeError: failure inside iterator
102108
Test #35 - Error handler #1 called with err = TypeError: failure inside iterator
103-
Test #40 - Promise.resolve calls thenable.then
104-
Test #41 - thenable.then resolve = function reject = function
105-
Test #43 - Success handler #1 called with result = '' (length = 0) (isArray = true)
106-
Test #44 - Success handler called with result = 'tampered' (length = 1) (isArray = true)
107-
Test #45 - Success handler called with result = '' (length = 1) (isArray = true)
108-
Test #46 - Success handler #1a called with result = '2' (isArray = false) (fulfillCalled = false)
109-
Test #48 - Success handler #1 called with res = 'success'
110-
Test #49 - Error handler #1 called with err = 'success'
111-
Test #50 - Success handler #1 called with res = 'success'
112-
Test #51 - Error handler #1 called with err = 'success'
109+
Test #41 - Promise.resolve calls thenable.then
110+
Test #42 - thenable.then resolve = function reject = function
111+
Test #44 - Success handler #1 called with result = '' (length = 0) (isArray = true)
112+
Test #45 - Success handler called with result = 'tampered' (length = 1) (isArray = true)
113+
Test #46 - Success handler called with result = '' (length = 1) (isArray = true)
114+
Test #47 - Success handler #1a called with result = '2' (isArray = false) (fulfillCalled = false)
115+
Test #49 - Success handler #1 called with res = 'success'
116+
Test #50 - Error handler #1 called with err = 'success'
117+
Test #51 - Success handler #1 called with res = 'success'
113118
Test #52 - Error handler #1 called with err = 'success'
114-
Test #53 - Success handler #1 called with res = 'success'
115-
Test #54 - Success finally handler called for resolved promise without value
116-
Test #55 - Success finally handler called for rejected promise without value
117-
Test #62 - val is 0(Expect 0)
118-
Test #65 - Success - []
119-
Test #67 - Failed - {}
119+
Test #53 - Error handler #1 called with err = 'success'
120+
Test #54 - Success handler #1 called with res = 'success'
121+
Test #55 - Success finally handler called for resolved promise without value
122+
Test #56 - Success finally handler called for rejected promise without value
123+
Test #63 - val is 0(Expect 0)
124+
Test #66 - Success - []
125+
Test #68 - Failed - {}
120126
Test #6 - Error handler #2 called with err = thenable.get:error!
121127
Test #8 - Error handler #2 called with err = success.throw:error
122128
Test #9 - Error handler #2 called with err = error.throw:error
@@ -140,37 +146,37 @@ Test #38 - p1 success: p1
140146
Test #38 - p2 success: p2
141147
Test #38 - p3 success: p3
142148
Test #39 - Success handler #1 called with result = success value 1,42,TypeError: an error
143-
Test #40 - Promise.resolve call nested within thenable.then = nested Promise.resolve call
144-
Test #46 - Success handler #1b called with result = '3' (isArray = false) (fulfillCalled = false)
145-
Test #47 - Error handler #1 called with err = 2
146-
Test #58 - Success - Rejected status and value passed through finally with not callable argument
147-
Test #59 - Success - Resolved status and value passed through finally with not callable argument
148-
Test #60 - Success - own rejection passed through finally
149+
Test #41 - Promise.resolve call nested within thenable.then = nested Promise.resolve call
150+
Test #47 - Success handler #1b called with result = '3' (isArray = false) (fulfillCalled = false)
151+
Test #48 - Error handler #1 called with err = 2
152+
Test #59 - Success - Rejected status and value passed through finally with not callable argument
153+
Test #60 - Success - Resolved status and value passed through finally with not callable argument
149154
Test #61 - Success - own rejection passed through finally
150-
Test #63 - p1 success: p1
151-
Test #63 - p4 failure: p4
152-
Test #64 - p1 failure: p1
153-
Test #67 - p1 success: p1
154-
Test #67 - p2 success: p2
155-
Test #67 - p3 success: p3
155+
Test #62 - Success - own rejection passed through finally
156+
Test #64 - p1 success: p1
157+
Test #64 - p4 failure: p4
158+
Test #65 - p1 failure: p1
156159
Test #68 - p1 success: p1
157-
Test #69 - Success - [{"status":"fulfilled","value":"p1"},{"status":"fulfilled","value":"p2"}]
160+
Test #68 - p2 success: p2
161+
Test #68 - p3 success: p3
162+
Test #69 - p1 success: p1
163+
Test #70 - Success - [{"status":"fulfilled","value":"p1"},{"status":"fulfilled","value":"p2"}]
158164
Test #7 - Error handler #2 called with err = thenable.call:error!
159165
Test #13 - Catch handler #2 called with err = error2
160166
Test #15 - Catch handler #1 called with err = failure
161167
Test #27 - Success handler #1 called with result = p1
162168
Test #28 - Error handler #1 called with err = p1
163169
Test #37 - Error handler #1 called with err = p3
164170
Test #38 - Success handler #1 called with result = p1,p2,p3
165-
Test #42 - Catch handler #1 called with err = expected1
166-
Test #42 - Catch handler #2 called with err = expected2
167-
Test #42 - Catch handler #3 called with err = expected3
168-
Test #63 - Success - [{"status":"fulfilled","value":"p1"},{"status":"fulfilled","value":"p2"},{"status":"rejected","reason":"p3"},{"status":"rejected","reason":"p4"}]
169-
Test #64 - Success - [{"status":"rejected","reason":"p1"},{"status":"rejected","reason":"p2"},{"status":"rejected","reason":"p3"}]
170-
Test #68 - Success: [{"status":"fulfilled","value":"p1"}]
171-
Test #68 - p1 success: p2
172-
Test #68 - p1 failure: e2
171+
Test #43 - Catch handler #1 called with err = expected1
172+
Test #43 - Catch handler #2 called with err = expected2
173+
Test #43 - Catch handler #3 called with err = expected3
174+
Test #64 - Success - [{"status":"fulfilled","value":"p1"},{"status":"fulfilled","value":"p2"},{"status":"rejected","reason":"p3"},{"status":"rejected","reason":"p4"}]
175+
Test #65 - Success - [{"status":"rejected","reason":"p1"},{"status":"rejected","reason":"p2"},{"status":"rejected","reason":"p3"}]
176+
Test #69 - Success: [{"status":"fulfilled","value":"p1"}]
177+
Test #69 - p1 success: p2
178+
Test #69 - p1 failure: e2
173179
Test #4 - Success handler #2 called with result = chain:success2
174-
Test #56 - Success - Rejected status and value passed through finally
175-
Test #57 - Success - Resolved status and value passed through finally
176-
Test #46 - Success handler #2 called with result = '0,,4' (length = 3) (isArray = true) (fulfillCalled = true)
180+
Test #57 - Success - Rejected status and value passed through finally
181+
Test #58 - Success - Resolved status and value passed through finally
182+
Test #47 - Success handler #2 called with result = '0,,4' (length = 3) (isArray = true) (fulfillCalled = true)

test/es6/ES6PromiseAsync.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -882,6 +882,20 @@ var tests = [
882882
);
883883
}
884884
},
885+
{
886+
name: "Promise.all called with empty iterator, calls Promise.resolve synchronously and passes abrupt completion to reject handler",
887+
body: function (index) {
888+
function FakePromise(fn) {
889+
function resolve() { echo(`Test #${index} - resolve called`); throw new Error('oops'); }
890+
function reject(e) { echo(`Test #${index} - reject called: ${e.message}`) }
891+
fn(resolve, reject);
892+
this.then = function(onResolve, onReject) {};
893+
}
894+
895+
FakePromise.resolve = function() {};
896+
Promise.all.call(FakePromise, []);
897+
}
898+
},
885899
{
886900
name: "Promise.resolve called with a thenable calls then on the thenable",
887901
body: function (index) {
@@ -1310,6 +1324,20 @@ var tests = [
13101324
e => echo(`Test #${index} - Failed - ${JSON.stringify(e)}`));
13111325
}
13121326
},
1327+
{
1328+
name: "Promise.allsettled called with empty iterator, calls Promise.resolve synchronously and passes abrupt completion to reject handler",
1329+
body: function (index) {
1330+
function FakePromise(fn) {
1331+
function resolve() { echo(`Test #${index} - resolve called`); throw new Error('oops'); }
1332+
function reject(e) { echo(`Test #${index} - reject called: ${e.message}`) }
1333+
fn(resolve, reject);
1334+
this.then = function(onResolve, onReject) {};
1335+
}
1336+
1337+
FakePromise.resolve = function() {};
1338+
Promise.allSettled.call(FakePromise, []);
1339+
}
1340+
},
13131341
];
13141342

13151343
var index = 1;

0 commit comments

Comments
 (0)