From 5eee87c45961d66bf378887f92ab05d92852f680 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Sun, 19 Oct 2025 20:57:03 +0300 Subject: [PATCH 1/2] fix(mapAsyncIterable): always close source when mapped iterable is thrown Even when the underlying source does not implement a `.throw()` method. --- .../__tests__/mapAsyncIterable-test.ts | 42 +++++++++++++++++++ src/execution/mapAsyncIterable.ts | 25 ++++++----- 2 files changed, 57 insertions(+), 10 deletions(-) diff --git a/src/execution/__tests__/mapAsyncIterable-test.ts b/src/execution/__tests__/mapAsyncIterable-test.ts index ac3753c9b4..8237582c58 100644 --- a/src/execution/__tests__/mapAsyncIterable-test.ts +++ b/src/execution/__tests__/mapAsyncIterable-test.ts @@ -264,6 +264,48 @@ describe('mapAsyncIterable', () => { await expectPromise(thrown).toRejectWith(message); }); + it('close source when mapped iterable is thrown even when the underlying source does not implement a throw method', async () => { + const items = [1, 2, 3]; + let returned = false; + const iterable: AsyncIterableIterator = { + [Symbol.asyncIterator]() { + return this; + }, + next() { + if (returned) { + return Promise.resolve({ done: true, value: undefined }); + } + const value = items[0]; + items.shift(); + return Promise.resolve({ + done: items.length === 0, + value, + }); + }, + return: () => { + returned = true; + return Promise.resolve({ done: true, value: undefined }); + }, + }; + + const doubles = mapAsyncIterable(iterable, (x) => x + x); + + expect(await doubles.next()).to.deep.equal({ value: 2, done: false }); + expect(await doubles.next()).to.deep.equal({ value: 4, done: false }); + + // Throw error + const message = 'allows throwing errors when mapping async iterable'; + const thrown = doubles.throw(new Error(message)); + await expectPromise(thrown).toRejectWith(message); + + // Returns early when throwing errors through async iterable + expect(returned).to.equal(true); + expect(await doubles.next()).to.deep.equal({ + value: undefined, + done: true, + }); + }); + it('passes through caught errors through async generators', async () => { async function* source() { try { diff --git a/src/execution/mapAsyncIterable.ts b/src/execution/mapAsyncIterable.ts index d85c7c4959..72494ff230 100644 --- a/src/execution/mapAsyncIterable.ts +++ b/src/execution/mapAsyncIterable.ts @@ -30,17 +30,19 @@ export function mapAsyncIterable( try { return { value: await callback(value), done: false }; } catch (error) { - /* c8 ignore start */ - // FIXME: add test case - if (typeof iterator.return === 'function') { - try { - await iterator.return(); - } catch (_e) { - /* ignore error */ - } - } + await returnIgnoringErrors(); throw error; - /* c8 ignore stop */ + } + } + + async function returnIgnoringErrors(): Promise { + if (typeof iterator.return === 'function') { + try { + await iterator.return(); /* c8 ignore start */ + } catch (_error) { + // FIXME: add test case + /* ignore error */ + } /* c8 ignore stop */ } } @@ -57,6 +59,9 @@ export function mapAsyncIterable( async throw(error?: unknown) { if (typeof iterator.throw === 'function') { return mapResult(iterator.throw(error)); + } else if (typeof iterator.return === 'function') { + await returnIgnoringErrors(); + throw error; } throw error; }, From 91eb03471e9205f91cce15391bd77bb9d621a0ad Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Thu, 23 Oct 2025 23:08:34 +0300 Subject: [PATCH 2/2] tweak branches --- src/execution/mapAsyncIterable.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/execution/mapAsyncIterable.ts b/src/execution/mapAsyncIterable.ts index 72494ff230..e3511bc4c4 100644 --- a/src/execution/mapAsyncIterable.ts +++ b/src/execution/mapAsyncIterable.ts @@ -59,10 +59,12 @@ export function mapAsyncIterable( async throw(error?: unknown) { if (typeof iterator.throw === 'function') { return mapResult(iterator.throw(error)); - } else if (typeof iterator.return === 'function') { + } + + if (typeof iterator.return === 'function') { await returnIgnoringErrors(); - throw error; } + throw error; }, [Symbol.asyncIterator]() {