Skip to content
Domphy

Error Handling & Not Found

Handling loader errors

@domphy/router is headless — it does not render components automatically. When a loader throws, the match transitions to status: "error" and the error is available on the match. Check this in your UI:

import { createRoute } from "@domphy/router"

const postRoute = createRoute({
  getParentRoute: () => rootRoute,
  path: "/posts/$id",
  loader: async ({ params }) => {
    const post = await fetchPost(params.id)
    if (!post) throw new Error("Post not found")
    return post
  },
})

In your UI layer, read match.status and match.error:

const PostView = {
  div: (l) => {
    const match = matches.get(l).find((m) => m.routeId === postRoute.id)
    if (!match) return []
    if (match.status === "error") return [
      { h2: "Something went wrong" },
      { p: (match.error as Error).message },
      { button: "Go back", onClick: () => history.back() },
    ]
    if (match.status === "pending") return [{ p: "Loading…" }]
    return [PostPage(match.loaderData)]
  },
}

Not-found errors

When a resource doesn't exist (404-equivalent), throw notFound() from a loader. The match transitions to status: "notFound":

import { createRoute, notFound } from "@domphy/router"

const userRoute = createRoute({
  getParentRoute: () => rootRoute,
  path: "/users/$id",
  loader: async ({ params }) => {
    const user = await fetchUser(params.id)
    if (!user) throw notFound()
    return user
  },
})

In your UI, check match.status === "notFound" to render the not-found state.

Global not-found route

Catch all unmatched paths:

const rootRoute = createRootRoute({
  notFoundComponent: () => ({
    div: [
      { h1: "404 — Page not found" },
      { p: "The page you're looking for doesn't exist." },
      { a: "Back to home", href: "/" },
    ],
    style: { padding: "4rem", textAlign: "center" },
  }),
})

Or use createRoute with path *:

const catchAll = createRoute({
  getParentRoute: () => rootRoute,
  path: "*",
})

Error vs. not-found — when to use which

ScenarioUse
Server error (5xx), network failurethrow new Error(...)errorComponent
Resource doesn't exist (404)throw notFound()notFoundComponent
User not authorized (401/403)throw redirect(...) → redirects to login
Missing required search paramthrow new Error(...) or throw notFound() depending on UX

Retrying after error

The reset function in errorComponent re-runs the route:

const errorBoundary = ({ error, reset }) => ({
  div: [
    { p: `Error: ${(error as Error).message}` },
    {
      button: "Retry",
      onClick: async () => {
        // Optional: clear the cache before retrying
        await client.invalidateQueries()
        reset()
      },
    },
  ],
})

Handling loader errors globally

Log all route errors centrally:

const router = createRouter({
  routeTree,
  onError: (error) => {
    console.error("Route error:", error)
    errorReporter.capture(error)
  },
})

TypeScript: typed errors

When your loaders throw typed errors, narrow in errorComponent:

class ApiError extends Error {
  constructor(public status: number, message: string) {
    super(message)
  }
}

const route = createRoute({
  loader: async () => {
    const res = await fetch("/api/data")
    if (!res.ok) throw new ApiError(res.status, await res.text())
    return res.json()
  },
  errorComponent: ({ error }) => {
    if (error instanceof ApiError) {
      return { div: `API Error ${error.status}: ${error.message}` }
    }
    return { div: "Unknown error" }
  },
})

Pending component during error recovery

While a retry is in flight, show a pending state:

const route = createRoute({
  pendingComponent: () => ({ div: "Retrying…" }),
  pendingMinMs: 200,
  errorComponent: ({ reset }) => ({
    div: [
      { p: "Failed to load. " },
      { button: "Retry", onClick: reset },
    ],
  }),
})