Welcome to Chapter 17! So far, we’ve built a solid understanding of the TanStack ecosystem, leveraging its powerful tools to manage state, build dynamic UIs, and handle complex data flows. We’ve created features, optimized performance, and made our applications interactive. But what happens when things go wrong? How do we ensure our code is reliable, and how do we get it into the hands of users efficiently?

This chapter is all about getting your TanStack-powered application ready for the real world. We’ll dive into the critical aspects of production readiness: implementing robust error handling to gracefully manage unexpected issues, strategizing effective testing to catch bugs before they reach users, and understanding modern deployment practices to get your application live. By the end of this chapter, you’ll have a comprehensive toolkit to build not just functional, but also resilient and deployable TanStack applications.

To get the most out of this chapter, it’s helpful to be familiar with the core concepts from previous chapters, especially those on TanStack Query for data fetching, TanStack Router for navigation, and general React component development. We’ll be building on that foundation to integrate these crucial production-grade practices. Let’s make our applications rock-solid!

Core Concepts: Building Resilient Applications

Building an application that works perfectly in your development environment is one thing; ensuring it performs flawlessly and handles unexpected scenarios gracefully in production is another. This section will explore the fundamental concepts of error handling, testing, and deployment within the TanStack ecosystem.

1. Robust Error Handling

Errors are an inevitable part of software development. How we anticipate, catch, and respond to them defines the robustness and user experience of our applications. TanStack libraries provide excellent mechanisms for managing errors, especially with asynchronous operations.

TanStack Query Error Management

TanStack Query (latest stable v5.x as of 2026-01-07) is designed with error handling in mind for its asynchronous data operations. When a query or mutation fails, TanStack Query provides immediate feedback and mechanisms to react.

  • The error Property: Every useQuery hook returns an error property. This property will be null if there’s no error, or it will contain the error object if the query failed. You can check isError or error directly to conditionally render UI.
  • onError Callbacks: You can define onError callbacks both at the individual query/mutation level and globally on the QueryClient. This is perfect for logging errors, showing toast notifications, or triggering side effects.
  • useErrorBoundary Option: For more severe errors that should halt rendering a component tree, TanStack Query offers a useErrorBoundary option. When set to true, if a query errors, it will throw the error, allowing a React Error Boundary higher up in the component tree to catch it. This is a powerful pattern for isolating failures.
  • retry Logic: TanStack Query automatically retries failed queries by default. You can configure the number of retries or disable them entirely, which is crucial for handling transient network issues.
TanStack Router Error Handling

TanStack Router (latest stable v1.x as of 2026-01-07) also provides mechanisms to handle errors that occur during route loading or rendering.

  • errorComponent: Each route definition can specify an errorComponent. If any data loading (loader function) or rendering within that route’s component tree throws an error, the errorComponent for that route (or a parent route) will be rendered instead. This allows you to show specific error messages for different parts of your application.
  • Global Error Handling: You can define a top-level errorComponent in your Router instance to catch errors not handled by more specific route error components, providing a consistent fallback for unexpected issues.
React Error Boundaries

At a foundational level, React (latest stable v19.x as of 2026-01-07) provides Error Boundaries. These are React components that catch JavaScript errors anywhere in their child component tree, log those errors, and display a fallback UI instead of crashing the entire application. They are crucial for catching rendering errors that useQuery’s useErrorBoundary option propagates.

Let’s visualize how these error handling mechanisms can work together:

flowchart TD A[User Action] --> B{API Request?} B -->|Yes| C[Call `useQuery`] C --> D{API Fails?} D -->|Yes| E[`useQuery` reports `error`] E --> F[Trigger `onError` callback] E --> G{`useErrorBoundary` enabled?} G -->|Yes| H[Error thrown by `useQuery`] H --> I{React Error Boundary catches?} I -->|Yes| J[Display Fallback UI] I -->|No| K[Application Crash] G -->|No| L[Render component `error` prop] L --> M[Conditional UI `isError` / `error`] N[Route Navigation] --> O[TanStack Router `loader`] O --> P{`loader` fails?} P -->|Yes| Q[Router renders `errorComponent`] Q --> J O -->|No| R[Route renders normally]

Figure 17.1: Integrated Error Handling Flow in a TanStack Application

This diagram illustrates how a user action might trigger an API request via useQuery. If that request fails, useQuery can either report the error directly to the component (M) or throw it (H) for a React Error Boundary (J) to catch. Similarly, if a loader function in TanStack Router fails during navigation (P), the Router can render a specific errorComponent (Q). The goal is to gracefully degrade, inform the user, and prevent a complete application crash.

2. Effective Testing Strategies

Testing is paramount for ensuring the quality and stability of your application. With the TanStack ecosystem, we often deal with asynchronous data, routing, and complex UI interactions, which require specific testing approaches.

Unit Testing TanStack Components and Hooks
  • Custom Hooks with useQuery: For custom hooks that wrap useQuery, you’ll want to test their behavior in various states (loading, success, error). Tools like @testing-library/react-hooks (or renderHook from @testing-library/react for newer versions) are invaluable here. You’ll often mock your API calls using a library like Mock Service Worker (MSW) or simple Jest mocks to control the data returned.
  • Individual Components: Test your presentational components in isolation, ensuring they render correctly based on props, including different states of TanStack Query data (e.g., isLoading, isError, data).
Integration Testing
  • TanStack Router: Test navigation flows, ensuring routes load correctly, loaders fetch data as expected, and URL parameters are handled properly. You might use @testing-library/react to render your application with a test router and simulate user interactions.
  • TanStack Table/Form: Test complex interactions within your tables and forms. For tables, this means testing sorting, filtering, pagination, and row selection. For forms, it’s about validating input, submission, and error display. Again, MSW can simulate backend responses for form submissions.
Mocking API Calls with MSW

Mock Service Worker (MSW) is a powerful tool for intercepting network requests at the service worker level (in browsers) or Node.js level (in tests). This allows you to define mock responses for your API endpoints, making your tests fast, reliable, and independent of an actual backend server. It’s highly recommended for testing useQuery and useMutation hooks.

3. Deployment Best Practices

Once your application is robust and well-tested, the final step is to deploy it. Modern frontend deployment involves considerations like build processes, hosting, and performance optimizations.

  • Build Process: Use your framework’s build tools (e.g., Vite, Next.js, Remix) to compile your TypeScript/JavaScript, optimize assets (CSS, images), and generate a production-ready bundle. TanStack libraries are designed to be tree-shakable, meaning unused code is removed during the build, resulting in smaller bundles.
  • Client-Side Hydration (for SSR/SSG): If you’re using a framework like TanStack Start (which builds on Vite and offers SSR/SSG capabilities), you’ll leverage client-side hydration. This is where the server renders the initial HTML, and then the client-side JavaScript “takes over” and makes the page interactive. TanStack Query’s dehydrate/hydrate functions are crucial here for transferring server-fetched data to the client without refetching.
  • Environment Variables: Manage sensitive information (API keys, backend URLs) using environment variables. Your build process should correctly inject these into your application based on the deployment environment (development, staging, production).
  • Performance Optimizations:
    • Code Splitting: Break your application’s JavaScript bundle into smaller chunks that are loaded on demand. TanStack Router naturally supports code splitting for routes.
    • Caching: Configure proper HTTP caching headers for your static assets. TanStack Query handles its own data caching, but browser caching for your application’s code is also important.
    • CDN (Content Delivery Network): Host your static assets on a CDN for faster delivery to users worldwide.
  • Monitoring and Logging: Integrate tools to monitor your application’s performance, user behavior, and error rates in production. Services like Sentry for error tracking, or application performance monitoring (APM) tools, are essential. Your onError callbacks in TanStack Query are perfect places to send errors to these services.

Step-by-Step Implementation: Production-Ready Features

Let’s put these concepts into practice. We’ll add error handling to a useQuery hook and demonstrate a basic test setup.

Step 1: Enhancing Error Handling in TanStack Query

Imagine you have a component that fetches a list of products. We’ll enhance its error handling.

First, let’s set up a simple QueryClient and a component.

// src/queryClient.ts
import { QueryClient } from '@tanstack/react-query';

export const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      // Default options for all queries
      staleTime: 1000 * 60 * 5, // 5 minutes
      gcTime: 1000 * 60 * 60,   // 1 hour
      retry: 3,                 // Retry failed queries 3 times
    },
  },
});

This queryClient.ts defines our QueryClient with some sensible defaults, including retries.

Now, let’s create a product fetching component.

// src/api/products.ts
export interface Product {
  id: string;
  name: string;
  price: number;
}

// Simulate an API call that might fail
const fetchProducts = async (): Promise<Product[]> => {
  const shouldFail = Math.random() > 0.7; // 30% chance of failure
  if (shouldFail) {
    throw new Error("Failed to fetch products! Network issue or server error.");
  }
  return new Promise((resolve) =>
    setTimeout(() => {
      resolve([
        { id: "1", name: "Laptop", price: 1200 },
        { id: "2", name: "Mouse", price: 25 },
        { id: "3", name: "Keyboard", price: 75 },
      ]);
    }, 500)
  );
};

export { fetchProducts };

This fetchProducts function simulates an API call, with a random chance of failure.

Now, in your component:

// src/components/ProductList.tsx
import React from 'react';
import { useQuery } from '@tanstack/react-query';
import { fetchProducts, Product } from '../api/products';

function ProductList() {
  const { data, isLoading, isError, error } = useQuery<Product[], Error>({
    queryKey: ['products'],
    queryFn: fetchProducts,
    // Add an onError callback for specific logging or side effects
    onError: (err) => {
      console.error("ProductList: Error fetching products:", err.message);
      // Here you might send this error to an error tracking service like Sentry
    },
    // Setting useErrorBoundary to true will make this query throw its error
    // which can then be caught by a React Error Boundary higher up the tree.
    // useErrorBoundary: true,
  });

  if (isLoading) {
    return <p>Loading products...</p>;
  }

  if (isError) {
    // If useErrorBoundary is false (default), we handle the error here.
    return <p style={{ color: 'red' }}>Error: {error?.message}</p>;
  }

  return (
    <div>
      <h2>Our Products</h2>
      <ul>
        {data?.map((product) => (
          <li key={product.id}>
            {product.name} - ${product.price}
          </li>
        ))}
      </ul>
    </div>
  );
}

export default ProductList;

Explanation:

  1. We’ve imported useQuery and our fetchProducts function.
  2. The useQuery hook now explicitly types the potential error as Error.
  3. We added an onError callback. This callback fires after all retries have failed. It’s a great place to log errors to the console or send them to an external error tracking service.
  4. The isError and error properties are used to conditionally render an error message to the user.
  5. The useErrorBoundary: true option (currently commented out) demonstrates how you would tell TanStack Query to throw the error, allowing a React Error Boundary to catch it.

Step 2: Implementing a React Error Boundary

To gracefully catch errors thrown by components (or by useQuery when useErrorBoundary: true), we use a React Error Boundary.

// src/components/ErrorBoundary.tsx
import React, { Component, ErrorInfo, ReactNode } from 'react';

interface Props {
  children?: ReactNode;
  fallback?: ReactNode; // Optional fallback UI
}

interface State {
  hasError: boolean;
  error: Error | null;
}

class ErrorBoundary extends Component<Props, State> {
  public state: State = {
    hasError: false,
    error: null,
  };

  // This static method is called when an error is thrown. It returns an object
  // to update the state, indicating an error has occurred.
  public static getDerivedStateFromError(error: Error): State {
    return { hasError: true, error };
  }

  // This method is called after an error has been caught. It's used for
  // logging error information.
  public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
    console.error("Uncaught error:", error, errorInfo);
    // Here you would send error to a logging service (e.g., Sentry.captureException(error, { extra: errorInfo }))
  }

  public render() {
    if (this.state.hasError) {
      // You can render any custom fallback UI
      return this.props.fallback || (
        <div style={{ padding: '20px', border: '1px solid red', color: 'red' }}>
          <h1>Something went wrong!</h1>
          <p>We're sorry for the inconvenience. Please try refreshing the page.</p>
          {this.state.error && <p>Details: {this.state.error.message}</p>}
        </div>
      );
    }

    return this.props.children;
  }
}

export default ErrorBoundary;

Explanation:

  1. This is a standard React class component that implements getDerivedStateFromError to update its state when an error is thrown by a child.
  2. componentDidCatch is used for side effects like logging the error.
  3. When hasError is true, it renders a fallback UI instead of its children.
  4. You can wrap any part of your component tree with this ErrorBoundary.

Now, let’s wrap our ProductList component with this ErrorBoundary in App.tsx:

// src/App.tsx
import React from 'react';
import { QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; // v5.x
import { queryClient } from './queryClient';
import ProductList from './components/ProductList';
import ErrorBoundary from './components/ErrorBoundary'; // Import our ErrorBoundary

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <div style={{ padding: '20px' }}>
        <h1>TanStack Production Readiness Demo</h1>
        <ErrorBoundary> {/* Wrap the potentially erroring component */}
          <ProductList />
        </ErrorBoundary>

        {/* Example with useErrorBoundary: true
        <ErrorBoundary fallback={<div>Failed to load critical data!</div>}>
          <ProductListWithErrorBoundary /> // A version of ProductList with useErrorBoundary: true
        </ErrorBoundary>
        */}
      </div>
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  );
}

export default App;

To see this in action:

  1. Run your application.
  2. Refresh the page multiple times. Because fetchProducts has a 30% chance of failure, you should eventually see the error message rendered by ProductList (if useErrorBoundary is false) or the ErrorBoundary’s fallback UI (if useErrorBoundary is true in ProductList).

Step 3: Basic Testing with TanStack Query and MSW

Let’s write a simple test for our fetchProducts function and ProductList component using Jest and Mock Service Worker.

First, install necessary dependencies:

npm install --save-dev @testing-library/react @testing-library/jest-dom @tanstack/react-query-testing-library jest-environment-jsdom msw
# Or using yarn
yarn add --dev @testing-library/react @testing-library/jest-dom @tanstack/react-query-testing-library jest-environment-jsdom msw

Note: @tanstack/react-query-testing-library provides renderWithClient which simplifies testing useQuery hooks. For React 18+, renderHook from @testing-library/react is also a great option.

Configure MSW:

Create src/mocks/handlers.ts:

// src/mocks/handlers.ts
import { http, HttpResponse } from 'msw'; // v2.x

export const handlers = [
  http.get('/api/products', () => {
    return HttpResponse.json([
      { id: "test-1", name: "Test Product A", price: 100 },
      { id: "test-2", name: "Test Product B", price: 200 },
    ], { status: 200 });
  }),
  http.get('/api/products-error', () => {
    return HttpResponse.json({ message: "Internal Server Error" }, { status: 500 });
  }),
];

Create src/mocks/server.ts:

// src/mocks/server.ts
import { setupServer } from 'msw/node'; // v2.x
import { handlers } from './handlers';

export const server = setupServer(...handlers);

Configure Jest to use MSW:

// setupTests.ts (or similar file configured in Jest setupFilesAfterEnv)
import '@testing-library/jest-dom';
import { server } from './src/mocks/server';

// Establish API mocking before all tests.
beforeAll(() => server.listen());

// Reset any request handlers that we may add during the tests,
// so they don't affect other tests.
afterEach(() => server.resetHandlers());

// Clean up after the tests are finished.
afterAll(() => server.close());

Make sure your ProductList uses the /api/products endpoint. Let’s modify fetchProducts to use a relative path, and then Jest with MSW will intercept it.

// src/api/products.ts (updated)
export interface Product {
  id: string;
  name: string;
  price: number;
}

const fetchProducts = async (): Promise<Product[]> => {
  // Use a relative path so MSW can intercept
  const response = await fetch('/api/products');
  if (!response.ok) {
    throw new Error('Failed to fetch products');
  }
  return response.json();
};

export { fetchProducts };

Now, create src/components/ProductList.test.tsx:

// src/components/ProductList.test.tsx
import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import { QueryClientProvider } from '@tanstack/react-query';
import { QueryClient } from '@tanstack/react-query'; // Import QueryClient directly for tests
import ProductList from './ProductList';
import { server } from '../mocks/server'; // Import our MSW server
import { http, HttpResponse } from 'msw'; // v2.x

// Create a new QueryClient for each test to ensure isolation
const createTestQueryClient = () => new QueryClient({
  defaultOptions: {
    queries: {
      retry: false, // Disable retries in tests for faster feedback
    },
  },
});

describe('ProductList', () => {
  it('renders loading state initially', () => {
    // Render the component with a fresh QueryClient
    render(
      <QueryClientProvider client={createTestQueryClient()}>
        <ProductList />
      </QueryClientProvider>
    );
    expect(screen.getByText(/Loading products.../i)).toBeInTheDocument();
  });

  it('renders products on successful fetch', async () => {
    render(
      <QueryClientProvider client={createTestQueryClient()}>
        <ProductList />
      </QueryClientProvider>
    );

    // MSW will intercept the /api/products call and return mock data
    await waitFor(() => {
      expect(screen.getByText('Test Product A - $100')).toBeInTheDocument();
      expect(screen.getByText('Test Product B - $200')).toBeInTheDocument();
    });
    expect(screen.queryByText(/Loading products.../i)).not.toBeInTheDocument();
  });

  it('renders error message on failed fetch', async () => {
    // Override the default MSW handler for this specific test
    server.use(
      http.get('/api/products', () => {
        return HttpResponse.json({ message: "Network Error" }, { status: 500 });
      })
    );

    render(
      <QueryClientProvider client={createTestQueryClient()}>
        <ProductList />
      </QueryClientProvider>
    );

    await waitFor(() => {
      expect(screen.getByText(/Error: Failed to fetch products/i)).toBeInTheDocument();
    });
    expect(screen.queryByText(/Loading products.../i)).not.toBeInTheDocument();
  });
});

Explanation:

  1. We import render, screen, and waitFor from @testing-library/react.
  2. QueryClientProvider and QueryClient are used to provide the TanStack Query context to our component. We create a createTestQueryClient helper to ensure each test gets a fresh, isolated QueryClient, and we disable retries for faster test execution.
  3. beforeAll, afterEach, afterAll hooks (from setupTests.ts) manage the MSW server lifecycle.
  4. The first test verifies the isLoading state.
  5. The second test verifies successful data fetching. MSW intercepts the /api/products call and returns the mock data defined in handlers.ts. waitFor is used because the data fetching is asynchronous.
  6. The third test demonstrates how to override a handler for a specific test case, simulating an API error.

This setup provides a robust foundation for testing your TanStack Query-driven components, ensuring your data fetching logic and UI reactions are correct.

Mini-Challenge: Global Error Handling for useQuery

You’ve seen how to handle errors locally with onError and how useErrorBoundary works. Now, let’s make error handling even more consistent.

Challenge: Implement a global onError handler for all useQuery and useMutation calls within your application. This global handler should log a generic message and then re-throw the error (or call a global error reporting service).

Hint: The QueryClient constructor accepts defaultOptions.queries.onError and defaultOptions.mutations.onError.

What to observe/learn: How to centralize error logic, reducing boilerplate and ensuring consistent behavior across your application. This is especially useful for integrating with external error reporting tools.

Common Pitfalls & Troubleshooting

  1. Not Handling Loading/Error States Explicitly: A common mistake is assuming data will always be available. Always account for isLoading, isError, and isSuccess states in your UI. If you only render data when data is present, you might show a blank screen during loading or after an error.

    • Troubleshooting: Use conditional rendering or default values. if (isLoading) return <Spinner />; if (isError) return <ErrorMessage error={error} />; return <DataComponent data={data} />;
  2. Over-Mocking in Tests: While mocking is essential, over-mocking can make tests brittle. If you mock every single function call, your tests might pass even if the underlying implementation changes in a way that breaks real-world behavior.

    • Troubleshooting: Use MSW for network requests (integration level) and only mock specific utility functions or external libraries (unit level). Focus on testing component behavior, not the internal workings of useQuery itself.
  3. Incorrect Environment Variable Configuration in Deployment: Forgetting to set environment variables or setting them incorrectly for a production build can lead to your application failing to connect to APIs or using incorrect settings.

    • Troubleshooting: Double-check your CI/CD pipeline and hosting provider’s environment variable settings. Ensure your build process correctly injects these variables (e.g., VITE_APP_API_URL for Vite, NEXT_PUBLIC_API_URL for Next.js). Never commit sensitive keys directly to your repository.
  4. Forgetting to Hydrate State in SSR/SSG: When using Server-Side Rendering or Static Site Generation with TanStack Query, if you don’t dehydrate the QueryClient state on the server and rehydrate it on the client, the client-side application will refetch all data, leading to a flash of loading states and slower perceived performance.

    • Troubleshooting: Ensure your SSR/SSG setup correctly uses dehydrate on the server and hydrate on the client, passing the dehydrated state in the initial HTML response. TanStack Start handles this automatically for you.

Summary

Congratulations on completing Chapter 17! You’ve taken crucial steps towards building production-ready TanStack applications. Here’s a quick recap of the key takeaways:

  • Error Handling:
    • TanStack Query provides isError, error, onError, and useErrorBoundary for robust asynchronous error management.
    • TanStack Router offers errorComponent to gracefully handle routing and data loading failures.
    • React Error Boundaries are essential for catching rendering errors and providing fallback UIs.
  • Testing:
    • Use @testing-library/react and renderHook for unit testing components and custom hooks.
    • Mock Service Worker (MSW) is the gold standard for mocking API requests in tests, ensuring reliability and speed.
    • Always aim for a balance between unit and integration tests.
  • Deployment:
    • Understand your build process and leverage optimizations like tree-shaking and code splitting.
    • Properly manage environment variables for different deployment environments.
    • Utilize client-side hydration for SSR/SSG to avoid unnecessary data refetches.
    • Implement monitoring and logging solutions to track application health in production.

By diligently applying these practices, you’re not just writing code; you’re building reliable, maintainable, and user-friendly applications that can stand the test of time in a production environment.

What’s next? With a solid understanding of production readiness, you’re well-equipped to tackle even more advanced topics. The final chapters will likely delve into more sophisticated architectural patterns, performance deep-dives, or even explore how to extend TanStack libraries with custom plugins and integrations. Stay curious and keep building!

References


This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.