ErrorBoundary

Stage: implementation

NOTE

ErrorBoundary is experimental and currently a technical preview. Its API and behavior may still change as we gather feedback from real-world usage.

ErrorBoundary renders fallback$ instead of its children when a descendant throws.

It catches during SSR, during out-of-order streaming and on the client, and it composes with Suspense in either order.

To use it, add experimental: ['errorBoundary'] to your qwikVite plugin options:

// vite.config.ts
import { defineConfig } from 'vite';
import { qwikVite } from '@qwik.dev/core/optimizer';
 
export default defineConfig(() => {
  return {
    plugins: [
      qwikVite({
        experimental: ['errorBoundary'],
      }),
    ],
  };
});
import { $, ErrorBoundary, component$ } from '@qwik.dev/core';
 
export default component$(() => {
  return (
    <ErrorBoundary
      fallback$={$((error, reset) => (
        <div role="alert">
          <p>{error.message}</p>
          <button onClick$={() => reset()}>Try again</button>
        </div>
      ))}
    >
      <Profile />
    </ErrorBoundary>
  );
});

Props

fallback$

Required. Receives the caught error and a reset QRL, and returns the JSX to render in place of the children.

fallback$: QRL<(error: Error & { digest?: string }, reset: QRL<() => void>) => JSXOutput>;

The error is always an Error, so {error.message} is safe: a value that is not an Error arrives wrapped, with the original on cause.

Wrap reset in a handler — onClick$={() => reset()}, not onClick$={reset} — so it stays wired in a streamed fallback.

onError$

Optional. A side effect for reporting; it never affects what renders.

onError$?: QRL<(error: Error, info: ErrorBoundaryInfo) => void>;

It receives the original error and an ErrorBoundaryInfo:

  • phase — where the error came from: 'render', 'task', 'event', 'async-generator' or 'async-signal'.
  • boundaryId — identifies the boundary within the page.
  • digest — the code a production fallback shows for this error, so a user's bug report matches your logs.

An error caught during SSR is reported again if the client re-derives it, so dedupe in your reporter.

Reset

reset clears the error and re-renders the children. If the condition that caused the throw is gone, the content comes back; if it still throws, the fallback renders again.

fallback$={$((error, reset) => (
  <button onClick$={() => reset()}>Retry</button>
))}

Production redaction

In production the fallback receives a generic message plus a digest, so server details never reach the browser. In development it receives the real error.

fallback$={$((error) => (
  <p>
    Something went wrong. {error.digest && <code>{error.digest}</code>}
  </p>
))}

Log info.digest from onError$ to match a reported code back to the error in your logs.

PublicError

Throw a PublicError for a message you intend the user to read. It renders unredacted, including in production.

import { PublicError, component$ } from '@qwik.dev/core';
 
export const Checkout = component$(() => {
  if (outOfStock) {
    throw new PublicError('This item is out of stock.');
  }
  return <p>In stock</p>;
});

Construction is consent, and it covers the whole instance — keep secrets off it. A string data doubles as the message; an object data with a string message field lifts it onto .message.

transformError

A server render option that projects a thrown error into the Error the fallback displays during SSR. onError$ still receives the original.

// entry.ssr.tsx
export default function (opts: RenderToStreamOptions) {
  return renderToStream(<Root />, {
    ...opts,
    transformError: (error) => (error instanceof HttpError ? new Error(error.publicMessage) : undefined),
  });
}

Return an Error to project it, or undefined/null to decline and fall through to the default policy. Any other return, or a throw, redacts to the generic error. It is called synchronously, so it cannot be async.