finally is not called when asynchronously iterating over synchronous generator which yields rejected promise
Categories
(Core :: JavaScript Engine, defect)
Tracking
()
People
(Reporter: v, Unassigned)
Details
Attachments
(1 file)
1.36 KB,
application/x-javascript
|
Details |
User Agent: Mozilla/5.0 (X11; Linux x86_64; rv:72.0) Gecko/20100101 Firefox/72.0
Steps to reproduce:
run the code:
void async function () {
try {
for await (const x of function*() {
try { yield Promise.reject("reject") }
finally { console.log('finally after reject') }
}()){
console.log(await x)
}
} catch (e) {
console.log(e)
}
}()
Actual results:
- prints "reject"
Expected results:
- prints "finally after reject"
- prints "reject"
If I first use babel to transpile asynchronous generators then the code works as expected.
If I change generator to asynchronous like
void async function () {
try {
for await (const x of async function*() {
try { yield Promise.reject("reject") }
finally { console.log("finally after reject") }
}()){
console.log(await x)
}
} catch (e) {
console.log(e)
}
}()
then the code works as expected.
If I change yielding rejected promise to throwing error then the code works as expected.
The same error occurs in node@10-12 and in Chrome.
Attached file contains different combinations of synchronous/asynchronous loop/generator. Only this case fails.
Comment 1•5 years ago
|
||
For throw
cases (1st, 3rd, 5th cases in the attachment),
they have nothing to do with for-loop.
Before leaving the sync/async generator by throw
,
the execution directly goes into finally
block,
and performs console.log("finally after throw")
.
So I'm not going to dive into the details.
About rejected promise cases, the behavior depends on when the rejected promise is converted into abrupt completion and how the abrupt completion gets handled.
a) for-of + sync generator + reject (2nd case in the attachment)
13.7.5.13 Runtime Semantics:
ForIn/OfBodyEvaluation ( lhs, stmt, iteratorRecord, iterationKind, lhsKind,
labelSet [ , iteratorKind ] )
...
6. Repeat,
a. Let nextResult be
? Call(iteratorRecord.[[NextMethod]], iteratorRecord.[[Iterator]]).
the sync generator yields a rejected promise,
and nextResult
becomes { value: Promise.reject("reject"), done: false }
and there's no special handling for the promise.
it gets assigned to x
variable in the code.
l. Let result be the result of evaluating stmt.
then, when performing the body (stmt
above), it does await x
where x
is the rejected promise,
and result
becomes abrupt completion ({ [[Type]]: throw, [[Value]]: "reject" }
)
n. If LoopContinues(result, labelSet) is false, then
i. If iterationKind is enumerate, then
1. Return Completion(UpdateEmpty(result, V)).
ii. Else,
1. Assert: iterationKind is iterate.
2. Set status to UpdateEmpty(result, V).
3. If iteratorKind is async, return
? AsyncIteratorClose(iteratorRecord, status).
4. Return ? IteratorClose(iteratorRecord, status).
Then, if the body returns abrupt completion, it performs IteratorClose
,
that calls return
on the generator.
that resumes the execution of the generator and jumps into finally
block,
and performs console.log("finally after throw")
.
b) for-await-of + async generator + reject (4th case in the attachment)
this also has nothing to do with the for-loop.
yield
in async generator implies await
.
https://tc39.es/ecma262/#sec-generator-function-definitions-runtime-semantics-evaluation
14.4.14 Runtime Semantics: Evaluation
YieldExpression : yield AssignmentExpression
1. Let generatorKind be ! GetGeneratorKind().
2. Let exprRef be the result of evaluating AssignmentExpression .
3. Let value be ? GetValue(exprRef).
4. If generatorKind is async, then return ? AsyncGeneratorYield(value).
...
https://tc39.es/ecma262/#sec-asyncgeneratoryield
25.5.3.7 AsyncGeneratorYield ( value )
...
5. Set value to ? Await(value).
and that results in abrupt completion, and before leaving the generator,
it goes into finally
block,
and performs console.log("finally after throw")
.
c) for-await-of + sync generator + reject (6th case in the attachment)
At the beginning of the loop, it creates async-from-sync iterator object
13.7.5.12 Runtime Semantics:
ForIn/OfHeadEvaluation ( TDZnames, expr, iterationKind )
...
6. If iterationKind is enumerate, then
...
7. Else,
a. Assert: iterationKind is iterate.
b. If iterationKind is async-iterate, let iteratorHint be async.
c. Else, let iteratorHint be sync.
d. Return ? GetIterator(exprValue, iteratorHint).
https://tc39.es/ecma262/#sec-getiterator
7.4.1 GetIterator ( obj [ , hint [ , method ] ] )
...
3. If method is not present, then
a. If hint is async, then
i. Set method to ? GetMethod(obj, @@asyncIterator).
ii. If method is undefined, then
1. Let syncMethod be ? GetMethod(obj, @@iterator).
2. Let syncIteratorRecord be ? GetIterator(obj, sync, syncMethod).
3. Return ! CreateAsyncFromSyncIterator(syncIteratorRecord).
So, in the loop body, it calls the async-from-sync itertor's next method.
13.7.5.13 Runtime Semantics:
ForIn/OfBodyEvaluation ( lhs, stmt, iteratorRecord, iterationKind, lhsKind,
labelSet [ , iteratorKind ] )
...
6. Repeat,
a. Let nextResult be
? Call(iteratorRecord.[[NextMethod]], iteratorRecord.[[Iterator]]).
https://tc39.es/ecma262/#sec-%asyncfromsynciteratorprototype%.next
25.1.4.2.1 %AsyncFromSyncIteratorPrototype%.next ( value )
...
1. Let O be the this value.
2. Assert: Type(O) is Object and O has a [[SyncIteratorRecord]] internal
slot.
3. Let promiseCapability be ! NewPromiseCapability(%Promise%).
4. Let syncIteratorRecord be O.[[SyncIteratorRecord]].
5. Let result be IteratorNext(syncIteratorRecord, value).
6. IfAbruptRejectPromise(result, promiseCapability).
7. Return ! AsyncFromSyncIteratorContinuation(result, promiseCapability).
there, result
is { value: Promise.reject("reject"), done: false }
,
and it gets passed to AsyncFromSyncIteratorContinuation
.
https://tc39.es/ecma262/#sec-asyncfromsynciteratorcontinuation
25.1.4.4 AsyncFromSyncIteratorContinuation ( result, promiseCapability )
1. Let done be IteratorComplete(result).
2. IfAbruptRejectPromise(done, promiseCapability).
3. Let value be IteratorValue(result).
4. IfAbruptRejectPromise(value, promiseCapability).
5. Let valueWrapper be PromiseResolve(%Promise%, value).
6. IfAbruptRejectPromise(valueWrapper, promiseCapability).
7. Let steps be the algorithm steps defined in Async-from-Sync Iterator Value
Unwrap Functions.
8. Let onFulfilled be ! CreateBuiltinFunction(steps, « [[Done]] »).
9. Set onFulfilled.[[Done]] to done.
10. Perform ! PerformPromiseThen(valueWrapper, onFulfilled, undefined,
promiseCapability).
11. Return promiseCapability.[[Promise]].
and at step 5
https://tc39.es/ecma262/#sec-promise-resolve
25.6.4.6.1 PromiseResolve ( C, x )
1. Assert: Type(C) is Object.
2. If IsPromise(x) is true, then
a. Let xConstructor be ? Get(x, "constructor").
b. If SameValue(xConstructor, C) is true, return x.
3. Let promiseCapability be ? NewPromiseCapability(C).
4. Perform ? Call(promiseCapability.[[Resolve]], undefined, « x »).
5. Return promiseCapability.[[Promise]].
it creates a promise object, and resolves it with the result.value
, that is Promise.reject("reject")
.
this results in rejecting the promise.
after some steps related to promise job handling, it gets passed back to 13.7.5.13 step 6.a.
13.7.5.13 Runtime Semantics:
ForIn/OfBodyEvaluation ( lhs, stmt, iteratorRecord, iterationKind, lhsKind,
labelSet [ , iteratorKind ] )
...
6. Repeat,
a. Let nextResult be
? Call(iteratorRecord.[[NextMethod]], iteratorRecord.[[Iterator]]).
b. If iteratorKind is async, then set nextResult to ? Await(nextResult).
nextResult
there becomes the rejected promise.
and at step 6.b, it awaits on nextResult
, and it results in abrupt completion (that throws "reject" string).
Given the step does ? Await(...)
, it leaves the loop immediately,
without performing IteratorClose
,
so the generator's execution doesn't get resumed after the first yield
.
I'm not sure if this is really an expected behavior tho, at least this is a possible spec issue, not implementation issue.
(in the spec, the iterated value gets closed only when the assignment or the loop body returns abrupt completion)
Comment 2•5 years ago
|
||
closing as INVALID, given there's nothing to do here unless the spec somehow gets changed.
Comment 3•5 years ago
|
||
I overlooked the one thing:
If I first use babel to transpile asynchronous generators then the code works as expected.
I'm not sure what actually happens here tho, if the 6th case in the attachment behaves differently there, it's babel's issue.
ecmascript issue:
Description
•