In the remix template, can we return our own JSX in ErrorBoundary?

I was reading the remix docs on error boundaries and how we can use them to render alternative UIs specifically for errors. For example, in one of my nested routes (which still uses authenticate.admin(request) in the loader), I would like to do something like this:

export async function loader() {
  const context = await authenticate.admin(request);
  if (badConditionIsTrue()) {
    throw new Response("Oh no! Something went wrong!", {
      status: 500,
    });
  }
}

export function ErrorBoundary() {
  const error = useRouteError();
  if (isRouteErrorResponse(error)) {
    // error.status = 500
    // error.data = "Oh no! Something went wrong!"
    return (
      <div className="error-container">
        <h1>{error.status} Error</h1>
        <p>{error.data}</p>
      </div>
    );
  }

  return (
    <div className="error-container">
      <h1>Unknown Error</h1>
      <p>Something unexpected happened. Please try again.</p>
    </div>
  );
}  

But the shopify docs (Shopify App package for Remix) say to use the following in your authenticated routes, to automatically set up the error and headers boundaries to redirect outside the iframe when needed (for auth errors):

import {boundary} from '@shopify/shopify-app-remix/server';

export function ErrorBoundary() {
  return boundary.error(useRouteError());
}

export const headers = (headersArgs) => {
  return boundary.headers(headersArgs);
};

This seems to imply we can’t return our own JSX in the error boundary like in a regular remix app to display user friendly error messages (unless it is an unauthenticated route).

This is the source code from shopify for boundary.error by the way (github):

export function errorBoundary(error: any) {
  if (
    error.constructor.name === 'ErrorResponse' ||
    error.constructor.name === 'ErrorResponseImpl'
  ) {
    return (
      <div
        dangerouslySetInnerHTML={{__html: error.data || 'Handling response'}}
      />
    );
  }

  throw error;
}

So this will always catch a thrown Response (which is what is idiomatically used in remix for throwing expected errors as shown in my example above).

Hi @neutroware

For authenticated routes you’ll need to use boundary.error(useRouteError()) in the error boundary - custom JSX for thrown Response errors can’t be used in this context. However for unauthenticated routes you can use a custom error boundary with your own JSX.

Okay, thank you @Liam-Shopify . I suppose I will just fall back to the more classic pattern of error handling where we conditionally render different UIs within the component itself (for expected errors). For example:

import { json } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import { Banner } from "@shopify/polaris"; // or your own

export async function loader() {
  const context = await authenticate.admin();
  const bad = await badConditionIsTrue();
  if (bad) {
    // 200 + a typed "presentable" error shape
    return json({
      ok: false as const,
      error: { kind: "KNOWN_BAD_STATE", message: "Something needs your attention." }
    });
  }
  return json({ ok: true as const, data: {/* ... */} });
}

export default function Screen() {
  const data = useLoaderData<typeof loader>();
  if (!data.ok) {
    return (
      <div className="page">
        <Banner status="critical" title="Heads up">
          {data.error.message}
        </Banner>
      </div>
    );
  }
  return <YourNormalUI data={data.data} />;
}

At least in this case I can easily display Polaris components in my error UIs.