@@ -513,14 +513,12 @@ jsg::Ref<ReadableStream> ReadableStream::pipeThrough(
513513
514514 auto options = kj::mv (maybeOptions).orDefault ({});
515515 options.pipeThrough = true ;
516+ // The lambda intentionally captures self as a visitable reference, ensuring
517+ // JSG_THIS stays alive until the pipe promise resolves.
516518 controller.pipeTo (js, destination, kj::mv (options))
517519 .then (js,
518520 JSG_VISITABLE_LAMBDA (
519- (self = JSG_THIS), (self), (jsg::Lock& js) { return js.resolvedPromise (); }),
520- JSG_VISITABLE_LAMBDA ((self = JSG_THIS), (self),
521- (jsg::Lock& js, auto && exception) {
522- return js.rejectedPromise <void >(kj::mv (exception));
523- }))
521+ (self = JSG_THIS), (self), (jsg::Lock& js) { return js.resolvedPromise (); }))
524522 .markAsHandled (js);
525523 return kj::mv (transform.readable );
526524}
@@ -583,11 +581,16 @@ jsg::Promise<void> ReadableStream::returnFunction(
583581 if (!state.preventCancel ) {
584582 auto promise = reader->cancel (js, value.map ([&](jsg::Value& v) { return v.getHandle (js); }));
585583 reader->releaseLock (js);
586- return promise.then (js,
584+ auto result = promise.then (js,
587585 JSG_VISITABLE_LAMBDA ((reader = kj::mv (reader)), (reader), (jsg::Lock& js) {
588586 // Ensure that the reader is not garbage collected until the cancel promise resolves.
589587 return js.resolvedPromise ();
590588 }));
589+ // When the stream is already errored, cancel() returns a rejected promise
590+ // that propagates through the .then() chain. Mark it as handled so V8 does
591+ // not fire unhandledrejection events during iterator teardown.
592+ result.markAsHandled (js);
593+ return kj::mv (result);
591594 }
592595
593596 reader->releaseLock (js);
0 commit comments