JORVEL

Error & 404 pages

Every app generated by jorvel generate host|remote ships three files that handle runtime failure and unmatched routes. They are part of your app — not framework dependencies — so editing them is the supported customization path.

Why ship these by default?

Production teams need a branded crash screen and 404 page before launch. Defaults cover both with sensible behavior (full stack in dev, brand-safe message in prod) so you can ship Day 1 and refine later.

Scaffolded files

text
apps/<name>/
└── src/
    ├── error-boundary.tsx     # Top-level React error boundary
    └── pages/
        ├── _error.tsx          # Crash screen — dev stack vs prod message
        ├── _404.tsx            # 404 page rendered for unknown URLs
        └── README.md           # How to override

For --lang js apps the extensions are .jsx instead of .tsx; the rest is identical.

The error boundary

src/error-boundary.tsx is a small class component that catches synchronous render errors anywhere in the tree below it and renders the local ErrorPage from pages/_error.tsx. The generated bootstrap.tsx wraps the entire app in this boundary.

src/bootstrap.tsx
tsx
ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <ErrorBoundary>
      <App />
    </ErrorBoundary>
  </React.StrictMode>
);

What the boundary does not catch

  • Promise rejections (async fetch failures, dynamic imports).
  • Errors thrown inside event handlers.
  • Errors during server-side rendering (caught by the SSR adapter instead).

For unhandled promise rejections, attach a global listener:

ts
window.addEventListener('unhandledrejection', (event) => {
  // Forward to your observability sink, e.g. @jorvel/observability.
  console.error(event.reason);
});

The default error page

pages/_error.tsx renders one of two layouts based on process.env.NODE_ENV:

  • Development — full error.message as the headline, formatted error.stack in a monospace block, dark/red theme to match standard dev-tools UX. A Try again button calls the reset() prop supplied by the boundary; Go home navigates back to /.
  • Production — generic, brand-safe message ("We hit an unexpected error"), no stack, no internals. Still keeps the reset + go-home affordances.

Inline stack only in dev

Stack traces can leak server paths and module ids. JORVEL never renders them when NODE_ENV === 'production'. If you build a custom error page, preserve that branch.

The default 404 page

pages/_404.tsx exports NotFoundPage, which takes an optional path prop and renders a centered layout with the offending URL, a "go home" button, and a link back to the docs.

The generated bootstrap.tsx includes a helper matchesAnyHostRoute(pathname, routes) that walks jorvel.routes.host.json and renders NotFoundPage when no route would handle the current URL:

src/bootstrap.tsx
tsx
function App() {
  const pathname = usePathname();

  if (pathname === '/' || pathname === '') {
    return <Welcome defaultProjectName="shell" />;
  }

  if (!matchesAnyHostRoute(pathname, HOST_ROUTES)) {
    return <NotFoundPage path={pathname} />;
  }

  return <RemoteOutlet routes={HOST_ROUTES} remotes={REMOTES} />;
}

Override patterns

Edit the file directly

Both files are plain React components with zero JORVEL imports. Open src/pages/_error.tsx or src/pages/_404.tsx and rewrite the JSX. Nothing else in the framework needs to change.

Swap the component via boundary prop

Keep _error.tsx for the dev surface and supply a different component for production:

tsx
import { ErrorBoundary } from './error-boundary';
import { BrandedCrashScreen } from './crash-screen';

<ErrorBoundary
  fallback={process.env.NODE_ENV === 'production' ? BrandedCrashScreen : undefined}
>
  <App />
</ErrorBoundary>

Disable the boundary

Remove the <ErrorBoundary> wrapper in bootstrap.tsx to fall back to React's default behavior (white screen + console error in production). Recommended only when you have a higher-level boundary (e.g. supplied by the SSR adapter) handling the same scope.

Custom 404 logic

The default matchesAnyHostRoute uses prefix matching with /* suffix support. To handle more complex matching (regex, parameter capture beyond /*), replace the helper inline or build your own and render <NotFoundPage> from there.

Testing the pages

Throw inside any rendered component to trigger the boundary:

tsx
function BombButton() {
  return (
    <button onClick={() => { throw new Error('Manual test'); }}>
      Break the app
    </button>
  );
}

Visit a URL that no host route handles to trigger the 404:

text
http://localhost:3000/this-path-does-not-exist

See also

  • Observability — wire onError + RUM beacon so caught errors land in Sentry/OTel/console.
  • Routing — host vs remote sub-paths.
  • SSR — server-side error handling for renderRouteToString / renderRouteToStream.