remix-run / remix

Build Better Websites. Create modern, resilient user experiences with web fundamentals.
https://remix.run
MIT License
29.86k stars 2.52k forks source link

Error tracking for errors in `defer` #7671

Open hanayashiki opened 1 year ago

hanayashiki commented 1 year ago

What version of Remix are you using?

1.19.0

Are all your remix dependencies & dev-dependencies using the same version?

Steps to Reproduce

// routes/stream-error.ts

import { Suspense } from "react";

import { defer } from "@remix-run/node";
import { Await, useLoaderData } from "@remix-run/react";

export const loader = async ({}) => {
  return defer({
    data: (async () => {
      await new Promise((r) => setTimeout(r, 1000));

      throw new Error("dev stream error");
    })(),
  });
};

export default function DevStreamError() {
  const { data } = useLoaderData<typeof loader>();

  return (
    <div>
      test
      <Suspense fallback={"loading"}>
        <Await resolve={data}>{() => "loaded"}</Await>
      </Suspense>
    </div>
  );
}
// entry.server.ts

export function handleError(
  error: unknown,
  { request }: DataFunctionArgs,
): void {
  console.log("handleError in entry.server.ts");
  if (error instanceof Error) {
    captureRemixServerException(error, "remix.server", request);
  } else {
    // Optionally capture non-Error objects
    captureException(error);
  }
}

Expected Behavior

Maybe handleError should be invoked? Or any another handleDeferError function should be invoked?

I am currently unable to do anything for errors thrown in defer promises, unless I wrap them one by one with some custom error handling logics.

This problem is not discussed as far as I know. How do people track issues in defer?

ErrorBoundary doesn't help, because in production mode all error stack information invisible to client, let alone there is loss of information if not tracked on server.

Actual Behavior

handleError is not invoked for any error in defer

hanayashiki commented 1 year ago

Until there's an official support, I'm using this hack to catch all errors in defer promises.

This might break for any minor versions as some properties are not documented.

        /**
         * Hacking into the internal of remix's DeferredData to track their errors.
         * @see: https://github.com/remix-run/remix/issues/7671
         * @see: https://github.com/remix-run/react-router/blob/37c5f3c610d429439c5d79bf9f789451a1caa024/packages/router/utils.ts#L1300
         */
        if ("subscribe" in (data as any)) {
          const asDeferred: DeferredData = data as any;

          const unsub = asDeferred.subscribe((aborted, settledKey) => {
            if (aborted || (asDeferred as any)["pendingKeysSet"].size === 0) {
              unsub();
            }

            if (settledKey && !aborted) {
              const settledData = asDeferred.data[settledKey] as any;
              if (settledData["_error"]) {
                captureException(settledData["_error"]);
              }
            }
          });
        }