Handling Errors
Houdini provides a dedicated +error.tsx+error.jsx file for route-level error handling. When present alongside a +page.tsx+page.jsx, Houdini automatically wraps the page in an error boundary that catches errors thrown by the page, its queries, and any child routes nested below that directory.
import type { ErrorProps } from './$types'
export default function DashboardError({ errors, children }: ErrorProps) { return ( <div> <h1>Something went wrong</h1> <p>{errors[0].message}</p> </div> )}export default function DashboardError({ errors, children }) { return ( <div> <h1>Something went wrong</h1> <p>{errors[0].message}</p> </div>)}The errors prop is Array<Error | GraphQLError>. It will contain whichever error was thrown, whether that’s a network failure, a GraphQL response error, or anything else thrown inside the page tree.
The children prop
The error component also receives children, the page component that failed. You can render it if you want to show partial content alongside the error, but in most cases you won’t. It’s there when you need it.
Query data
A +error.tsx+error.jsx has access to the same layout queries that are in scope at its directory level, since those queries succeeded for the boundary to even render. These are passed as props just like they would be to +page.tsx+page.jsx. The type for all available props is ErrorProps, exported from ./$types.
import type { ErrorProps } from './$types'
// RootQuery results are available because the root layout query succeededexport default function RootError({ RootQuery, errors }: ErrorProps) { return ( <div> <p>Welcome, {RootQuery.viewer.name}, but something went wrong.</p> <p>{errors[0].message}</p> </div> )}// RootQuery results are available because the root layout query succeededexport default function RootError({ RootQuery, errors }) { return ( <div> <p>Welcome, {RootQuery.viewer.name}, but something went wrong.</p> <p>{errors[0].message}</p> </div>)}Scope
Error boundaries nest the same way layouts do. A +error.tsx+error.jsx catches errors from its sibling +page.tsx+page.jsx and all routes below it. A layout query error at the same level bubbles up and would be caught by an error boundary at the parent directory instead.
src/routes/ +error.tsx ← catches errors from the root page and all child routes +layout.gql dashboard/ +error.tsx ← catches errors from +page.tsx and child routes within dashboard/ +page.tsx +page.gqlGraphQL errors
By default Houdini throws GraphQL response errors so they’re catchable by the error boundary. The thrown value is unwrapped into the errors array, so each entry is a GraphQLError object with at least a message field (and optionally path, locations, and extensions). Learn how to type the extensions field.
SSR and query errors
Error boundaries catch GraphQL errors correctly during client-side navigation. Direct page loads (SSR) are different: Houdini streams the page shell to the browser immediately while queries are in flight. Components waiting on data are suspended and retried asynchronously once the response arrives. If a query returns an error during that retry, React is already mid-stream and cannot route the error to a class-based error boundary, so the server returns a 500 and the browser shows a blank error page.
Routing errors (notFound(), redirect(), etc.) are not affected because they throw synchronously before any suspension happens.
The recommended fix is to add a @loading directive to your page query. This tells Houdini to generate a Suspense boundary in the right place (between the error boundary and the suspended component) so React can hand off errors correctly during SSR. It also gives the browser something to show while the query is in flight. Place it as high up in your component tree as makes sense: the goal is to suspend on the minimum data needed for a usable first paint, so that lower-level errors have a boundary to land on.
query DashboardQuery @loading { viewer { name }}With @loading in place, Houdini generates a loading state component from your +page.tsx+page.jsx (see the loading states guide for how to define it). React sends that loading state in the initial HTML, holds the stream open until the query resolves, and either replaces it with the page content or lets the error reach the nearest +error.tsx+error.jsx. It’s important to note that when you do this, your application will return a status code 200 even if an error is bubbled up to the error state. This is just how React’s current implementation of streaming SSR works. There’s no way around it.
Client-side navigations don’t need any of this; the error boundary catches query errors there regardless.
Routing utilities
Houdini exports a set of functions you can call from any page component to signal a specific HTTP outcome. Each one throws an error that is caught by the nearest +error.tsx+error.jsx boundary.
import { notFound, unauthorized, forbidden, httpError, redirect } from '$houdini'| Function | Throws | HTTP status |
|---|---|---|
notFound() | RoutingError(404) | 404 |
unauthorized() | RoutingError(401) | 401 |
forbidden() | RoutingError(403) | 403 |
httpError(status) | RoutingError(status) | any status |
redirect(status, url) | RedirectError | 3xx |
The errors prop in your +error.tsx+error.jsx will contain the thrown instance. Use isRoutingError and isApiError to branch on error type:
import type { ErrorProps } from './$types'import { isRoutingError, isApiError } from '$houdini'
export default function ShowError({ errors }: ErrorProps) { const routing = errors.find(isRoutingError) if (routing?.status === 404) return <p>Show not found.</p> if (routing?.status === 401) return <p>Please log in.</p>
const api = errors.find(isApiError) if (api) return <p>{api.graphqlErrors[0].message}</p>
return <p>Something went wrong.</p>}import { isRoutingError, isApiError } from '$houdini'
export default function ShowError({ errors }) { const routing = errors.find(isRoutingError) if (routing?.status === 404) return <p>Show not found.</p> if (routing?.status === 401) return <p>Please log in.</p>
const api = errors.find(isApiError) if (api) return <p>{api.graphqlErrors[0].message}</p>
return <p>Something went wrong.</p>}404 and static URL matching
When no exact route matches the incoming URL, Houdini automatically finds the deepest layout whose static URL prefix matches, renders its +error.tsx+error.jsx as the 404 page, and returns HTTP 404 before streaming begins. No catch-all route needed.
redirect()
redirect() works on both the server and the client. On an initial SSR response where the redirect can be detected before streaming begins, Houdini returns an HTTP 3xx response with the Location header set. On the client, or when the redirect is detected mid-stream, it triggers a router navigation to the target URL.
import { redirect } from '$houdini'import type { PageProps } from './$types'
export default function DashboardPage({ ViewerQuery }: PageProps) { if (!ViewerQuery.viewer) { return redirect(302, '/login') } return <div>Welcome, {ViewerQuery.viewer.name}</div>}import { redirect } from '$houdini'
export default function DashboardPage({ ViewerQuery }) { if (!ViewerQuery.viewer) { return redirect(302, '/login') } return <div>Welcome, {ViewerQuery.viewer.name}</div>}