Menu

Guides

View as Markdown

Error handling

Use this page when you need to answer "what does the user actually see when this fails?" Kovo has different paths for a query-backed region, a typed mutation failure, a route outcome, and an unexpected request-shell exception.

Wrap a region#

Start with the smallest region-level fallback:

tsx
// Source-verified shape from packages/core/src/index.ts
import { ErrorBoundary, component } from '@kovojs/core';

declare function ProductGrid(): string;
declare function ProductGridError(): string;

export const CatalogPage = component({
  render: () => (
    <ErrorBoundary fallback={<ProductGridError />}>
      <ProductGrid />
    </ErrorBoundary>
  ),
});

This is the right tool when one part of the page can fail without taking down the whole document.

Run it#

Make the loader or render path throw inside that region. The boundary fallback renders in the same spot, and the rest of the document keeps its ordinary shell.

Use route-level outcomes when the whole page should change shape instead:

ts
import { notFound, route } from '@kovojs/server';

export const accountRoute = route('/account/:id', {
  async page({ params }) {
    if (params.id === 'missing') return notFound();
    return <main>Account</main>;
  },
});

Add the production shape#

Mutation failures are their own path. Expected form errors stay typed and local to the submitted form; they are not exceptions:

tsx
// Source-verified shape from packages/core/src/index.ts
import { FieldError, FormError, component } from '@kovojs/core';

declare const saveProfile: unknown;

export const ProfileForm = component({
  render: () => (
    <form mutation={saveProfile}>
      <input name="displayName" />
      <FieldError name="displayName" />
      <FormError code="DUPLICATE_NAME">That name is taken.</FormError>
    </form>
  ),
});

Unexpected enhanced-mutation failures use the browser runtime's response posture:

  • 401 with Kovo-Reauth redirects through the re-auth path.
  • 403 stays an authorization failure.
  • 500 becomes a render error, not a fake typed form failure.

For document-level shells, configure them once on the app:

ts
import { defineKovo } from '@kovojs/server';

const ErrorShell = () => 'Server error';
const ForbiddenShell = () => 'Forbidden';
const NotFoundShell = () => 'Not found';

const app = defineKovo({
  errorShells: { forbidden: ForbiddenShell, notFound: NotFoundShell, serverError: ErrorShell },
});

export default app.assemble({});

Handle failure#

Choose the narrowest failure surface that matches the app:

  • Use <ErrorBoundary> for one query-backed or render-heavy region.
  • Use context.fail(...), <FieldError>, and <FormError> for expected form failures.
  • Use notFound() or a guard outcome when the whole route should change shell.
  • Use app errorShells and onError for unexpected request-shell failures.

Observe it in production#

The request shell gives you one hook for unexpected exceptions:

ts
import { defineKovo } from '@kovojs/server';

const app = defineKovo({
  onError(error, context) {
    console.error('request failed', context.operation, error);
  },
});

export default app.assemble({});

Keep that hook for logging and reporting. It is not the place to invent alternate response bodies.

Next#

Spec & diagnostics

ErrorBoundary, FieldError, and FormError: packages/core/src/index.ts and packages/server/src/jsx-runtime.ts. Route outcomes such as notFound(): site/content/guides/routing.md and the server routing surface. App error shells and onError: packages/server/src/app-types.ts, packages/server/src/app.ts, and site/content/guides/request-shell.md. Enhanced mutation reauth handling lives in packages/browser/src/mutation-fetch.ts and the wire contract in spec/09-wire-protocol.md.

API reference: @kovojs/core, @kovojs/server.