Closed Bug 1610315 Opened 5 years ago Closed 5 years ago

finally is not called when asynchronously iterating over synchronous generator which yields rejected promise

Categories

(Core :: JavaScript Engine, defect)

72 Branch
defect
Not set
normal

Tracking

()

RESOLVED INVALID

People

(Reporter: v, Unassigned)

Details

Attachments

(1 file)

1.36 KB, application/x-javascript
Details
Attached file iter_bug.js

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:

  1. prints "reject"

Expected results:

  1. prints "finally after reject"
  2. 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.

Component: Untriaged → JavaScript Engine
OS: Unspecified → Linux
Product: Firefox → Core
Hardware: Unspecified → x86_64
OS: Linux → Unspecified
Hardware: x86_64 → Unspecified

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)

https://tc39.es/ecma262/#sec-runtime-semantics-forin-div-ofbodyevaluation-lhs-stmt-iterator-lhskind-labelset

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

https://tc39.es/ecma262/#sec-runtime-semantics-forin-div-ofheadevaluation-tdznames-expr-iterationkind

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.

https://tc39.es/ecma262/#sec-runtime-semantics-forin-div-ofbodyevaluation-lhs-stmt-iterator-lhskind-labelset

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.

https://tc39.es/ecma262/#sec-runtime-semantics-forin-div-ofbodyevaluation-lhs-stmt-iterator-lhskind-labelset

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)

closing as INVALID, given there's nothing to do here unless the spec somehow gets changed.

Status: UNCONFIRMED → RESOLVED
Closed: 5 years ago
Resolution: --- → INVALID

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.

You need to log in before you can comment on or make changes to this bug.

Attachment

General

Creator:
Created:
Updated:
Size: