From 00367a044e69d41f9c6681df5e69d557e4c327ba Mon Sep 17 00:00:00 2001 From: LeSingh1 Date: Fri, 29 May 2026 17:21:14 -0700 Subject: [PATCH 1/2] fix(async): pooledMap surfaces errors thrown by the input iterable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The catch block in pooledMap's writer loop discarded the error that came out of `for await (const item of array)` and built an AggregateError populated solely from the executing transformations. When the iterable itself threw (and no transformation had rejected), that meant an empty `AggregateError.errors` — callers saw only the generic "Cannot complete the mapping" message with no way to recover the underlying cause (#6716). Bind the caught value as `iterError` and include it in `errors` if it isn't already there. Promise.race rejections from the executing pool are already surfaced via the existing allSettled walk, so the `includes` check prevents duplicates for the common transformation- rejection path. Adds a regression test covering the iterable-throws case and verifies the existing 'handles errors' test still produces exactly the expected pair of rejections. Fixes #6716 --- async/pool.ts | 9 ++++++++- async/pool_test.ts | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/async/pool.ts b/async/pool.ts index 95ca516b4c4b..2d136e9e865c 100644 --- a/async/pool.ts +++ b/async/pool.ts @@ -89,13 +89,20 @@ export function pooledMap( // Wait until all ongoing events have processed, then close the writer. await Promise.all(executing); writer.close(); - } catch { + } catch (iterError) { const errors = []; for (const result of await Promise.allSettled(executing)) { if (result.status === "rejected") { errors.push(result.reason); } } + // The catch fires both when a transformation rejects and when the + // input iterable itself throws. When it's a transformation rejection + // the same reason is already in `executing`, but when the iterable + // throws the original error would otherwise be swallowed, leaving + // callers with an empty AggregateError (see #6716). Add it only if + // it isn't already accounted for. + if (!errors.includes(iterError)) errors.push(iterError); writer.write(Promise.reject( new AggregateError(errors, ERROR_WHILE_MAPPING_MESSAGE), )).catch(() => {}); diff --git a/async/pool_test.ts b/async/pool_test.ts index feeefe5290b8..4009418a4a5e 100644 --- a/async/pool_test.ts +++ b/async/pool_test.ts @@ -2,6 +2,7 @@ import { delay } from "./delay.ts"; import { pooledMap } from "./pool.ts"; import { + assert, assertEquals, assertGreaterOrEqual, assertLess, @@ -78,6 +79,40 @@ Deno.test("pooledMap() returns ordered items", async () => { assertEquals(returned, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); }); +Deno.test( + "pooledMap() surfaces errors thrown by the input iterable (#6716)", + async () => { + const sentinel = new Error("Iterator failed on first step!"); + async function* errorThrowing() { + throw sentinel; + yield 1; + } + const results = pooledMap( + 2, + errorThrowing(), + (i: number) => Promise.resolve(i), + ); + let caught: unknown; + try { + for await (const _ of results) { + // drain + } + } catch (e) { + caught = e; + } + assert(caught instanceof AggregateError); + const ag = caught as AggregateError; + assertEquals( + ag.message, + "Cannot complete the mapping as an error was thrown from an item", + ); + // The previous behavior left `errors` empty; the iterable's error was + // swallowed. Now it must be present so callers can introspect it. + assertEquals(ag.errors.length, 1); + assertEquals(ag.errors[0], sentinel); + }, +); + Deno.test("pooledMap() checks browser compat", async () => { // Simulates the environment where Symbol.asyncIterator is not available const asyncIterFunc = ReadableStream.prototype[Symbol.asyncIterator]; From d5ee03f90fc615a4534190ddc0c5965a86898ef0 Mon Sep 17 00:00:00 2001 From: LeSingh1 Date: Fri, 29 May 2026 20:36:29 -0700 Subject: [PATCH 2/2] fix: silence require-yield lint on test generator that only throws --- async/pool_test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/async/pool_test.ts b/async/pool_test.ts index 4009418a4a5e..06eeba300a8b 100644 --- a/async/pool_test.ts +++ b/async/pool_test.ts @@ -83,9 +83,9 @@ Deno.test( "pooledMap() surfaces errors thrown by the input iterable (#6716)", async () => { const sentinel = new Error("Iterator failed on first step!"); - async function* errorThrowing() { + // deno-lint-ignore require-yield + async function* errorThrowing(): AsyncGenerator { throw sentinel; - yield 1; } const results = pooledMap( 2,