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', 'event', or 'hook' (task throws report 'hook').
  • 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

Redaction follows the error's origin. A server-origin error reaches the fallback as a generic message plus a digest — server details never leave the server. A client-origin error renders as thrown: its message already lives in the browser bundle. An SSR-caught error the client re-derives (reset, owner re-render) becomes client-origin and shows raw.

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. The digest is client-spoofable — branch on it for display, never for trust.

For a deliberate user-facing throw, use your own class and branch on it in the fallback:

export class DisplayError extends Error {}
 
fallback$={$((error) => (
  <p>{error instanceof DisplayError ? error.message : 'Something went wrong.'}</p>
))}

The same throw during SSR is server-origin and redacts, so keep display throws client-side — or render the message conditionally instead of throwing.

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.