On this page

Error Handling

Rendering errors can be caught by the nearest React Error Boundary  but a boundary's fallback only renders in the browser. To server-render the error UI itself, Rebask provides <NotFound> and <ServerError>, which render inline and set the response status when headers haven't been sent yet.

Two kinds of failures

FailureHandled byDefault
Not Found<NotFound> in a page or layout, or a catch-all not found page404 page
Unexpected errortry/catch in the page, or an <ErrorBoundary> client component500 page

<NotFound>

Return <NotFound> from a server component when its data is missing. The response gets a 404 status when headers haven't been sent yet:

import { NotFound } from "@rebask/core/components/errors"

const product = await getProduct(id)

if (!product) {
	return (
		<NotFound>
			<p>The product is not available.</p>
		</NotFound>
	)
}

A page that renders <NotFound> can still respond with a 200 if streaming already started, but it's cached so subsequent visits will see a 404. It also emits a noindex meta tag, and the response includes x-robots-tag: noindex when headers haven't been sent.

<NotFound> renders its children, its job is to mark the response as a 404 so it adds no wrapper element of its own.

Unmatched URLs

To render a custom 404 for URLs that match no page, add a catch-all page and mark it as a not-found route:

// app/[...slug]/page.tsx
export const notFound = true

export default function NotFoundPage() {
	return <p>Page not found</p>
}

export const notFound = true renders the page exactly like any other page. The only difference is caching: Rebask never writes a pathname entry for it, serving one shared shell for every URL plus one intermediate shell per prerenderParams combination.

The response always carries a 404 and x-robots-tag: noindex, the page can read matched params with routeParams(), and placing the catch-all deeper (like blog/[...slug]) limits it to that subtree.

<NotFound> and the notFound catch-all page solve different problems. <NotFound> is returned from a page that did match, when its data turns out to be missing, the page still renders and caches like normal. The catch-all page is the fallback for URLs that match no other page.

<ServerError>

Return <ServerError> from a server component when something fails and you want to render your own error UI. The response gets an error status (when headers haven't been sent yet), and the page stays out of the cache:

import { ServerError } from "@rebask/core/components/errors"

export default async function Page() {
	try {
		const product = await getProduct(id)
		return <Content product={product} />
	} catch (error) {
		console.error(error)
		return (
			<ServerError status={500}>
				<p>An error happened</p>
			</ServerError>
		)
	}
}

It receives status as a prop, which defaults to 500. Use another error status such as 503 when appropriate. It also emits a noindex meta tag.

<ServerError> renders its children, its job is to mark the response as an error so it adds no wrapper element of its own.

<ErrorBoundary>

<ErrorBoundary> handles rendering errors in the browser:

// app/layout.tsx
import { ErrorBoundary } from "@rebask/core/components/error-boundary"

<ErrorBoundary fallback={<p>Something went wrong</p>}>{children}</ErrorBoundary>

It does not catch errors from event handlers or code that runs outside rendering, such as timers or unawaited promises. Browser errors throw directly while server errors are serialized and sent to the browser so the nearest boundary catches both.

Users can recover from an error by navigating away, and revisiting a failed page retries it. Back and forward navigations restore history as it was.