Navigated to /docs/core/errors-and-code-splitting

Errors and code splitting

Recover from component render failures, load component code on demand, and choose ErrorBoundary, lazy, or Deferred correctly.

Choose the boundary by what is pending

  • Use a page error export for a contextual route-loader failure.

  • Use ErrorBoundary for a descendant component that throws while rendering.

  • Use lazy when the component implementation should come from a dynamic import.

  • Use Deferred or createDeferredValue when a promise-backed data value should render fallback UI and participate in progressive SSR.

  • Tavo.js does not publish a Suspense component. Use the primitive that owns the actual failure or asynchronous work.

Recover from descendant render errors

ErrorBoundary renders its children until a descendant render throws. It then renders a static fallback or calls a fallback function with the error. Static and progressive server rendering apply the same fallback contract.

  • Changing resetKey under Object.is clears a captured client error and retries the current children.

  • Changing resetKey does not fix the underlying state and does not reset an unrelated lazy-loader cache.

  • If rendering the fallback also fails, the error continues to the parent boundary or runtime error reporting.

  • A boundary is not a replacement for route error exports, rejected action state, or expected form validation.

  • Fallback UI should be accessible, concise, and offer only recovery that can actually change the failing condition.

TSX
tsximport { ErrorBoundary } from "@tavojs/core";

function ProjectSummary({
  project,
}: {
  project: { name: string } | null;
}) {
  if (!project) {
    throw new Error("Project data is unavailable.");
  }

  return <h2>{project.name}</h2>;
}

export function ProjectPanel({
  project,
  version,
}: {
  project: { name: string } | null;
  version: number;
}) {
  return (
    <ErrorBoundary
      resetKey={version}
      fallback={(error: unknown) => {
        const message =
          error instanceof Error ? error.message : "Project failed to render.";

        return <p role="alert">{message}</p>;
      }}
    >
      <ProjectSummary project={project} />
    </ErrorBoundary>
  );
}

Load component code on demand

lazy accepts a loader that resolves either a component or a module with a default component. Browser rendering starts one shared pending load, shows fallback UI, and rerenders mounted subscribers when the loader settles.

  • fallback receives idle or loading. errorFallback receives error status and the caught loader error.

  • If errorFallback is omitted, a failed load is thrown during the next render so the nearest ErrorBoundary can capture it.

  • preload deduplicates the active load and resolves to the loaded component. getStatus reports idle, loading, loaded, or error.

  • Synchronous SSR does not start the loader; it renders fallback. Call preload before rendering when loaded server output is required.

  • A successful load is cached on that lazy component definition. A failed definition stays in error state; create a new definition or reload the owning module for a real retry.

TSX
tsximport { lazy } from "@tavojs/core";
import type { PageProps } from "@tavojs/core/router";

type ReportsData = {
  points: number[];
};

const ReportsChart = lazy(
  () => import("../components/ReportsChart"),
  {
    fallback: ({ status }) => {
      return <p aria-busy="true">Chart {status}</p>;
    },
    errorFallback: ({ error }) => {
      const message =
        error instanceof Error ? error.message : "Chart code failed to load.";

      return <p role="alert">{message}</p>;
    },
  },
);

export async function load(): Promise<ReportsData> {
  return { points: [12, 18, 25] };
}

export default function ReportsPage({
  data,
}: PageProps<ReportsData>) {
  return (
    <main>
      <h1>Reports</h1>
      <ReportsChart points={data?.points ?? []} />
    </main>
  );
}

Preload only when the server needs loaded output

  • Call preload from an explicit server preparation path before render, not from the component render function.

  • When fallback HTML is acceptable, let SSR render it and allow the browser to start loading after hydration.

  • Preloading changes code availability, not Deferred data state or route-loader caching.

TSX
tsxconst InvoicePreview = lazy(
  () => import("../components/InvoicePreview"),
);

export async function prepareInvoicePreview() {
  await InvoicePreview.preload();
}

export function InvoiceSection() {
  return <InvoicePreview />;
}

Verify every state and recovery path

  • Render a throwing child on the server and in the browser; assert the boundary fallback receives the error.

  • Change resetKey with corrected child inputs and confirm the subtree renders again.

  • Hold a lazy loader pending and assert both idle/loading fallback behavior and final replacement.

  • Reject a lazy loader with and without errorFallback; verify the local fallback or parent ErrorBoundary owns the error.

  • Render lazy synchronously during SSR to confirm the loader is not called, then preload and confirm loaded output.

Look up exact public types

Follow linked API names to their canonical TypeScript declarations and package boundaries.