Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

Route Error Boundaries and Pending UI

Hello! Welcome to the next lesson in our course on transforming SPAs to SSR applications with Tanstack.

In our previous session, we focused on using route loader functions to fetch data on the server before a component renders. This is a fundamental pattern for building fast, SEO-friendly applications. However, real-world data fetching isn't always instantaneous or successful. Network requests can be slow, and APIs can fail.

Today, we'll address how to create a robust and user-friendly experience around these realities. Our goal is to implement route-level error boundaries and pending UI states. This means we'll learn how to show loading indicators for slow requests and display graceful error messages when things go wrong, preventing your application from crashing.

This lesson will cover:

  • Displaying a pendingComponent for loaders that take time to resolve.
  • Fine-tuning the pending state behavior to avoid UI flashes.
  • Catching errors from loaders with a route-level errorComponent.
  • Implementing retry logic within your error components.
  • Setting up default error handlers for the entire application.

1. Handling Pending States for Slow Loaders

When you navigate to a new route, its loader function begins executing. If this fetch is quick, the transition feels instant. But if it takes a few hundred milliseconds or more, the user is left waiting on the previous page with no feedback. This can make the application feel broken or unresponsive.

Tanstack Router provides an elegant solution: the pendingComponent. This is a component you specify on your route that will be rendered if the loader takes too long to resolve.

The following video provides a clear, practical demonstration of implementing both pending and error components. We'll start with the pending state.

Tanstack Router Tutorial - Routing, Lazy Loading, Data Fetching, Params...

This clip from PedroTech's "Tanstack Router Tutorial" shows how to add a pending component to a route definition. It's a great visual introduction to the concept.

Watch the pending component. Notice how the pending property is added to the createFileRoute configuration, alongside loader and component.

As you saw, adding a pending state is straightforward. Here’s a typical implementation:

// src/routes/posts/$postId.tsx
import { createFileRoute } from '@tanstack/react-router'
import { fetchPost } from './-api/posts' // Assuming fetch logic is elsewhere
import { PostComponent } from './-components/PostComponent'

// A simple loading component
function PostPending() {
  return <div>Loading post details...</div>
}

export const Route = createFileRoute('/posts/$postId')({
  loader: ({ params }) => fetchPost(params.postId),
  component: PostComponent,
  pendingComponent: PostPending, // This component renders during the load
})

Fine-Tuning the Pending UX

A key detail is that the pendingComponent does not render immediately. By default, Tanstack Router waits 1 second (defaultPendingMs) before showing it. This is an optimistic approach that avoids a jarring "flash" of a loading indicator for fast network requests.

Furthermore, once the pending component is shown, it will remain visible for at least 500 milliseconds (defaultPendingMinMs), even if the data arrives sooner. This prevents the loading indicator from appearing and disappearing too quickly.

You can override these defaults on a per-route basis. For a deeper understanding of these configuration options, the official documentation is the best source.

Data Loading | TanStack Router React Docs

The official Tanstack Router documentation provides precise details on configuring pending state behavior. Reading this will give you full control over the loading user experience.

Please read the following sections: Handling Slow Loaders: This provides context for why you'd need a pending component. Showing a pending component: This explains the pendingMs option. Avoiding Pending Component Flash: This explains the pendingMinMs option.

2. Implementing Route-Level Error Boundaries

Now, let's consider what happens when a loader fails. For example, an API might return a 404 "Not Found" or a 500 "Server Error". If you throw an error from your loader, Tanstack Router will catch it and look for an error handler.

The most common way to handle this is with an errorComponent, which acts as a React Error Boundary scoped to that specific route.

Let's return to the video to see this in action.

Tanstack Router Tutorial - Routing, Lazy Loading, Data Fetching, Params...

This next segment from the same video demonstrates how to define an error component that renders when the loader throws an error.

About 25 minutes in, watch the error demonstration. The video intentionally triggers an error to show how the router automatically renders the specified error component.

Building a Robust Error Component

The video shows a basic implementation. In a production application, you'll want a more robust error component that can display error details and offer the user a way to recover.

The errorComponent receives props, including the error object itself and a reset function. However, for errors originating from a loader, it's better to use the useRouter hook to invalidate the router's cache and trigger a full reload of the route data.

Here is a more complete example:

// src/routes/posts/$postId.tsx
import {
  createFileRoute,
  ErrorComponentProps,
  useRouter,
} from '@tanstack/react-router'

// ... loader and component definitions

// A more robust error component
function PostError({ error }: ErrorComponentProps) {
  const router = useRouter()

  // You can inspect the error type for custom messages
  // if (error instanceof NotFoundError) { ... }

  return (
    <div>
      <h2>Something went wrong!</h2>
      <p>{error.message}</p>
      <button onClick={() => router.invalidate()}>Try again</button>
    </div>
  )
}

export const Route = createFileRoute('/posts/$postId')({
  // ... loader and component
  errorComponent: PostError,
})

In this example, the "Try again" button calls router.invalidate(), which tells Tanstack Router to clear its cache and re-run the loader function for the current route.

Global vs. Local Error Handling

You can define an errorComponent on any route, including layout routes. If a child route throws an error and doesn't have its own errorComponent, the error will "bubble up" to the nearest parent route that does.

You can also define a defaultErrorComponent when you create your router instance. This serves as a global fallback for any error that isn't caught by a more specific, route-level boundary.

The official documentation provides the most thorough explanation of these error handling strategies.

Data Loading | TanStack Router React Docs

These documentation pages cover everything from the error lifecycle to implementing default and per-route error components. They are essential for building resilient applications.

In the "Data Loading" document, please read the entire Handling Errors section. It covers onError (for logging), onCatch, and errorComponent, including the reset prop and using router.invalidate(). Then, review the TanStack Start documentation on error boundaries for a framework-specific perspective.

Error Boundaries (React Start)

Read this short page to see how to configure the defaultErrorComponent in src/router.tsx (see the default configuration) and how to create a per-route override (see the route override), which reinforces the concepts from the previous reading.

Conclusion

You have now learned how to manage the two most common side-effects of asynchronous data fetching: delays and failures. By thoughtfully implementing pending and error states, you can create a seamless and resilient user experience.

Key Takeaways:

  • pendingComponent: Renders a loading UI if a route's loader exceeds the pendingMs threshold (default: 1000ms).
  • errorComponent: Acts as a route-level error boundary, catching errors thrown from loaders or during rendering.
  • Error Bubbling: Errors bubble up to parent routes, allowing you to create shared error UIs in your layouts.
  • defaultErrorComponent: A global fallback configured on the router instance for any unhandled errors.
  • Retry Logic: Use router.invalidate() within an errorComponent to provide a robust "try again" mechanism for users.

In our next lesson, we will continue to build upon our data loading skills. We will focus on how to access and validate URL search parameters in route loaders using a schema library like Zod. This will introduce another common source of errors—invalid user input—and give us another opportunity to apply the error boundary patterns we learned today.

Can't find a good explanation? Sign up and we'll make it for you

Sign up