Skip to content

renderToStream: an onCleanup that throws during signal abort crashes the server process #4029

Closed

Description

@lovrozagar

Describe the bug

If you pass signal to renderToStream and abort it while the stream is still pending (client closed the tab), the tree gets disposed inside the abort listener. If any onCleanup throws at that point, the error doesn't go to onError. It goes out of the EventTarget dispatch, and Node and Bun treat it as an uncaught exception, so the whole server process exits.

Wrapping controller.abort() in try/catch doesn't help, because an error thrown by an abort listener is reported, not rethrown to whoever called abort().

How I ran into it: TanStack Query's QueryClientProvider reads props.client in its cleanup. In my app that's a getter that looks up the current request with getRequestEvent(). The abort listener runs from the socket close, outside the request's async context, so the getter throws and the server dies on the first client that disconnects mid-stream. I fixed my side by aborting inside provideRequestEvent, but any other cleanup that throws will still take the process down.

Your Example Website or App

Single file repro below, no app needed.

Steps to Reproduce the Bug or Issue

  1. Save this as repro.mjs next to solid-js and @solidjs/web 2.0.0-rc.14:
import { createComponent, Loading, createMemo, onCleanup } from "solid-js";
import { renderToStream } from "@solidjs/web";

const controller = new AbortController();
const never = new Promise(() => {});
const stream = renderToStream(
	() => {
		onCleanup(() => {
			throw new Error("cleanup threw");
		});
		return createComponent(Loading, {
			fallback: "waiting",
			get children() {
				const value = createMemo(async () => never);
				return [() => value()];
			},
		});
	},
	{ signal: controller.signal, onError: (error) => console.log("onError saw", error.message) },
);
const { readable, writable } = new TransformStream();
stream.pipeTo(writable);
await readable.getReader().read();
controller.abort();
  1. Run NODE_ENV=production node repro.mjs
  2. The process exits with an uncaught Error: cleanup threw. onError is never called.

Same result on Bun 1.4.3.

Expected behavior

abandon() should catch errors thrown while disposing the root and report them through onError (or reportServerError), the same way it already handles serializer.close() a few lines below. A client disconnecting shouldn't be able to kill the server through cleanup code.

Platform

  • OS: Linux (Arch)
  • Runtime: Node 24.21.0, Bun 1.4.3
  • solid-js 2.0.0-rc.14, @solidjs/web 2.0.0-rc.14 (server production build)

Additional context

The listener is onAbort around line 2153 of @solidjs/web/dist/server.js in rc.14. Since abandon() is only reachable through module private symbols, the signal is the only way to stop a render from outside, so callers can't work around this.

Activity

  1. added a commit that references this issue on Oct 13, 2026
  2. ryansolid commented on Oct 13, 2026

    @ryansolid
    Member

    Fixed in #4034 (merged into next as c032934, ships in rc.16). abandon() now catches a cleanup that throws while the render is torn down, including on a signal abort. It reports the error through onError as { kind: "render", handling: "failed" }, or to console.error when no hook is set. The rest of the teardown still runs. Your repro is the regression test, stream-abort-cleanup-throw-4029.spec.tsx. Thanks for the clear report.

    — Claude via Claude Code


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions