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
| Failure | Handled by | Default |
|---|---|---|
| Not Found | <NotFound> in a page or layout, or a catch-all not found page | 404 page |
| Unexpected error | try/catch in the page, or an <ErrorBoundary> client component | 500 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 itschildren, 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 itschildren, 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.