Skip to content

Fix cached Promise result being reused across for…of loop iterations with await#2304

Merged
lahma merged 2 commits into
mainfrom
copilot/fix-jint-promise-loop-issue
Mar 3, 2026
Merged

Fix cached Promise result being reused across for…of loop iterations with await#2304
lahma merged 2 commits into
mainfrom
copilot/fix-jint-promise-loop-issue

Conversation

Copilot AI commented Mar 3, 2026

Copy link
Copy Markdown
Contributor

When await is used inside a for...of loop body, all iterations after the first return the same resolved value as the first iteration instead of re-evaluating per-iteration.

var urls = ["https://d", "https://e", "https://f"];
for (let url of urls) {
    text += await fetch_async(url); // returned "response for https://d" three times
}

Root causes (both in JintForInForOfStatement.BodyEvaluation)

  • Stale _completedAwaits cache: AsyncFunctionInstance._completedAwaits caches await results keyed by AST node. Since the same await expression node is shared across every loop iteration, the first iteration's result was returned for all subsequent ones. The other loop statement types (JintForStatement, JintWhileStatement, JintDoWhileStatement) already clear this cache at iteration boundaries — for...of did not.

  • Iterator state not preserved across suspension: When the async function suspended at an await in the loop body, no iterator state was saved (only generators saved ForOfSuspendData). On resume, HeadEvaluation created a fresh iterator from position 0, restarting the loop from the beginning rather than continuing from the suspended iteration.

Changes

  • JintForInForOfStatement.BodyEvaluation: At the top of the while loop, clear _completedAwaits for async functions when !_isResuming — identical to the pattern in the other loop statement implementations.
  • JintForInForOfStatement.BodyEvaluation: Before executing the loop body with a sync iterator, save ForOfSuspendData into the async function's suspend data dictionary (iterator position, current value, iteration env, accumulated value). On resume, ExecuteInternal finds this data via the existing suspendable.Data.TryGet path and restores the correct iteration state.
Original prompt

This section details on the original issue you should resolve

<issue_title>Jint returns cached first Promise result when awaiting inside a for…of loop</issue_title>
<issue_description>Version/platform/runtime used

Jint: 4.6.0
OS: Windows
.NET runtime: .NET 10 Console app

Describe the bug

When awaiting Promises inside a for…of loop in Jint, only the first awaited Promise resolves correctly. All subsequent awaited Promises inside the loop return the same result as the first iteration, even though each iteration uses a different input value. The same async function, when awaited sequentially outside the loop, returns the correct distinct results.

To Reproduce

The program below produces the incorrect output:

response for https://a
response for https://b
response for https://c
response for https://d
response for https://d   <-- incorrect
response for https://d   <-- incorrect
class Program
{
    static async Task Main(string[] args)
    {
        Jint.Engine jintEngine = new();
        var result = await jintEngine.EvaluateAsync(
            $$"""
            function fetch_async(url) {
                return new Promise((resolve) => {
                    resolve(`response for ${url}\n`);
                });
            }
            (async function() {
                var text = "";
                text += await fetch_async("https://a"); // WORKS
                text += await fetch_async("https://b"); // WORKS
                text += await fetch_async("https://c"); // WORKS
                var urls = ["https://d", "https://e", "https://f"];
                for (let url of urls) {
                    text += await fetch_async(url); // DOES NOT WORK, first fetch_async() result repeated three times
                }
                return text;
            })();
            """);
        System.Diagnostics.Trace.WriteLine(result?.ToString());
    }
}

Expected behavior

response for https://a
response for https://b
response for https://c
response for https://d
response for https://e
response for https://f

Additional context

If I wrap the await Promise inside a Promise chain, then it works, i.e.

//for (let url of urls) {
//    text += await fetch_async(url); // DOES NOT WORK, first fetch_async() result repeated three times
//}

var chain = Promise.resolve();
for (let url of urls) {
    chain = chain.then(async () => { text += await fetch_async(url); });
}
await chain;
```</issue_description>

## Comments on the Issue (you are @copilot in this section)

<comments>
</comments>

🔒 GitHub Advanced Security automatically protects Copilot coding agent pull requests. You can protect all pull requests by enabling Advanced Security for your repositories. Learn more about Advanced Security.

…for...of loop

Co-authored-by: lahma <171892+lahma@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix Jint returning cached Promise result in for…of loop Fix cached Promise result being reused across for…of loop iterations with await Mar 3, 2026
Copilot finished work on behalf of lahma March 3, 2026 10:42
@lahma
lahma marked this pull request as ready for review March 3, 2026 10:57
@lahma
lahma merged commit 46df2c8 into main Mar 3, 2026
4 checks passed
@lahma
lahma deleted the copilot/fix-jint-promise-loop-issue branch March 3, 2026 11:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Jint returns cached first Promise result when awaiting inside a for…of loop

2 participants