CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-react-query

Hooks for managing, caching and syncing asynchronous and remote data in React

Pending
Overview
Eval results
Files

error-handling.mddocs/

Error Handling

Error boundary integration and reset functionality for graceful error recovery. React Query provides utilities to integrate with React error boundaries and manage error states across queries and mutations.

Capabilities

QueryErrorResetBoundary Component

Error boundary context provider that manages error reset functionality across multiple queries.

/**
 * Provides error reset functionality to child components
 * @param props - Configuration with children and optional render function
 * @returns JSX element with error reset context
 */
function QueryErrorResetBoundary(
  props: QueryErrorResetBoundaryProps
): JSX.Element;

interface QueryErrorResetBoundaryProps {
  /** Child components or render function */
  children:
    | ((value: QueryErrorResetBoundaryValue) => React.ReactNode)
    | React.ReactNode;
}

interface QueryErrorResetBoundaryValue {
  /** Clear the reset flag */
  clearReset: () => void;
  /** Check if boundary is in reset state */
  isReset: () => boolean;
  /** Trigger a reset */
  reset: () => void;
}

Usage Examples:

import { QueryErrorResetBoundary } from "react-query";

// Basic error boundary integration
function App() {
  return (
    <QueryErrorResetBoundary>
      {({ reset }) => (
        <ErrorBoundary
          onReset={reset}
          fallbackRender={({ error, resetErrorBoundary }) => (
            <div>
              <h2>Something went wrong:</h2>
              <pre>{error.message}</pre>
              <button onClick={resetErrorBoundary}>Try again</button>
            </div>
          )}
        >
          <MainContent />
        </ErrorBoundary>
      )}
    </QueryErrorResetBoundary>
  );
}

// With react-error-boundary library
function AppWithErrorBoundary() {
  return (
    <QueryErrorResetBoundary>
      {({ reset }) => (
        <ErrorBoundary
          onReset={reset}
          FallbackComponent={ErrorFallback}
        >
          <QueryClientProvider client={queryClient}>
            <MyApp />
          </QueryClientProvider>
        </ErrorBoundary>
      )}
    </QueryErrorResetBoundary>
  );
}

function ErrorFallback({ error, resetErrorBoundary }: {
  error: Error;
  resetErrorBoundary: () => void;
}) {
  return (
    <div role="alert" className="error-fallback">
      <h2>Oops! Something went wrong</h2>
      <pre>{error.message}</pre>
      <button onClick={resetErrorBoundary}>
        Try again
      </button>
    </div>
  );
}

useQueryErrorResetBoundary Hook

Hook to access error boundary reset functionality.

/**
 * Access error reset boundary controls
 * @returns Object with reset control functions
 */
function useQueryErrorResetBoundary(): QueryErrorResetBoundaryValue;

Usage Examples:

import { useQueryErrorResetBoundary } from "react-query";

// Manual error reset
function ErrorRecoveryComponent() {
  const { reset, isReset, clearReset } = useQueryErrorResetBoundary();

  const handleRecovery = () => {
    // Clear any cached errors
    reset();
    
    // Perform additional recovery logic
    queryClient.refetchQueries();
    
    // Clear the reset flag
    clearReset();
  };

  return (
    <div>
      <button onClick={handleRecovery}>
        Recover from Errors
      </button>
      {isReset() && (
        <div className="recovery-notice">
          Recovery in progress...
        </div>
      )}
    </div>
  );
}

// Conditional rendering based on reset state
function ConditionalErrorHandling() {
  const { isReset } = useQueryErrorResetBoundary();

  if (isReset()) {
    return <div>Recovering from errors...</div>;
  }

  return <MainContent />;
}

Advanced Usage Patterns

Custom Error Boundary

Creating a comprehensive error boundary with React Query integration:

interface ErrorBoundaryState {
  hasError: boolean;
  error: Error | null;
  errorInfo: React.ErrorInfo | null;
}

class QueryErrorBoundary extends React.Component<
  { children: React.ReactNode; onError?: (error: Error, errorInfo: React.ErrorInfo) => void },
  ErrorBoundaryState
> {
  constructor(props: any) {
    super(props);
    this.state = {
      hasError: false,
      error: null,
      errorInfo: null
    };
  }

  static getDerivedStateFromError(error: Error): Partial<ErrorBoundaryState> {
    return {
      hasError: true,
      error
    };
  }

  componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
    this.setState({
      error,
      errorInfo
    });

    // Call custom error handler
    this.props.onError?.(error, errorInfo);

    // Log to error reporting service
    console.error('Query Error Boundary caught error:', error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return (
        <QueryErrorResetBoundary>
          {({ reset }) => (
            <div className="error-boundary">
              <h2>Something went wrong</h2>
              <details style={{ whiteSpace: 'pre-wrap' }}>
                <summary>Error Details</summary>
                {this.state.error?.toString()}
                <br />
                {this.state.errorInfo?.componentStack}
              </details>
              <div className="error-actions">
                <button
                  onClick={() => {
                    reset();
                    this.setState({
                      hasError: false,
                      error: null,
                      errorInfo: null
                    });
                  }}
                >
                  Try Again
                </button>
                <button onClick={() => window.location.reload()}>
                  Reload Page
                </button>
              </div>
            </div>
          )}
        </QueryErrorResetBoundary>
      );
    }

    return this.props.children;
  }
}

Query-Level Error Handling

Handling errors at the individual query level:

function QueryWithErrorHandling() {
  const {
    data,
    error,
    isError,
    refetch,
    isRefetching
  } = useQuery({
    queryKey: ['sensitive-data'],
    queryFn: fetchSensitiveData,
    retry: (failureCount, error) => {
      // Don't retry authentication errors
      if (error.status === 401) {
        return false;
      }
      // Retry server errors up to 3 times
      return failureCount < 3;
    },
    retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
    onError: (error) => {
      // Log error for analytics
      analytics.track('query_error', {
        queryKey: 'sensitive-data',
        error: error.message,
        status: error.status
      });

      // Handle specific error types
      if (error.status === 401) {
        // Redirect to login
        router.push('/login');
      } else if (error.status >= 500) {
        // Show server error toast
        toast.error('Server error occurred. Please try again.');
      }
    },
    useErrorBoundary: (error) => {
      // Only throw to error boundary for unexpected errors
      return error.status >= 500;
    }
  });

  // Handle error states in component
  if (isError && error.status < 500) {
    return (
      <div className="query-error">
        <h3>Unable to load data</h3>
        <p>{error.message}</p>
        <button 
          onClick={() => refetch()}
          disabled={isRefetching}
        >
          {isRefetching ? 'Retrying...' : 'Try Again'}
        </button>
      </div>
    );
  }

  return (
    <div>
      {data && <DataDisplay data={data} />}
    </div>
  );
}

Global Error Handling

Setting up global error handling across all queries and mutations:

// Global error handler
const handleGlobalError = (error: any, type: 'query' | 'mutation') => {
  // Log to error reporting service
  errorReportingService.captureException(error, {
    tags: {
      type,
      component: 'react-query'
    }
  });

  // Handle authentication errors globally
  if (error.status === 401) {
    // Clear user data and redirect to login
    queryClient.setQueryData(['user'], null);
    router.push('/login');
    return;
  }

  // Handle rate limiting
  if (error.status === 429) {
    toast.warning('Too many requests. Please wait a moment.');
    return;
  }

  // Handle server errors
  if (error.status >= 500) {
    toast.error('Server error occurred. Our team has been notified.');
    return;
  }

  // Handle network errors
  if (!navigator.onLine) {
    toast.warning('You appear to be offline. Please check your connection.');
    return;
  }
};

// QueryClient with global error handling
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      onError: (error) => handleGlobalError(error, 'query'),
      retry: (failureCount, error) => {
        // Global retry logic
        if (error.status === 401 || error.status === 403) {
          return false;
        }
        if (error.status >= 400 && error.status < 500) {
          return false;
        }
        return failureCount < 3;
      }
    },
    mutations: {
      onError: (error) => handleGlobalError(error, 'mutation'),
      retry: (failureCount, error) => {
        // Be more conservative with mutation retries
        if (error.status >= 400 && error.status < 500) {
          return false;
        }
        return failureCount < 1;
      }
    }
  }
});

Error Recovery Strategies

Implementing different error recovery strategies:

function useErrorRecovery() {
  const queryClient = useQueryClient();
  const { reset } = useQueryErrorResetBoundary();

  const recoverFromError = useCallback(async (strategy: 'soft' | 'hard' | 'selective') => {
    switch (strategy) {
      case 'soft': {
        // Just reset error boundary
        reset();
        break;
      }

      case 'hard': {
        // Clear all cache and reset
        queryClient.clear();
        reset();
        break;
      }

      case 'selective': {
        // Remove only failed queries
        queryClient.getQueryCache().getAll().forEach(query => {
          if (query.state.status === 'error') {
            queryClient.removeQueries({ queryKey: query.queryKey });
          }
        });
        reset();
        break;
      }
    }
  }, [queryClient, reset]);

  return { recoverFromError };
}

function ErrorRecoveryPanel() {
  const { recoverFromError } = useErrorRecovery();
  const [isRecovering, setIsRecovering] = useState(false);

  const handleRecovery = async (strategy: 'soft' | 'hard' | 'selective') => {
    setIsRecovering(true);
    try {
      await recoverFromError(strategy);
      toast.success('Recovery completed successfully!');
    } catch (error) {
      toast.error('Recovery failed. Please try again.');
    } finally {
      setIsRecovering(false);
    }
  };

  return (
    <div className="error-recovery-panel">
      <h3>Error Recovery Options</h3>
      <div className="recovery-buttons">
        <button 
          onClick={() => handleRecovery('soft')}
          disabled={isRecovering}
        >
          Soft Reset
        </button>
        <button 
          onClick={() => handleRecovery('selective')}
          disabled={isRecovering}
        >
          Clear Failed Queries
        </button>
        <button 
          onClick={() => handleRecovery('hard')}
          disabled={isRecovering}
        >
          Full Reset
        </button>
      </div>
      {isRecovering && <div>Recovering...</div>}
    </div>
  );
}

Error Monitoring and Analytics

Comprehensive error monitoring setup:

interface ErrorMetrics {
  totalErrors: number;
  queryErrors: number;
  mutationErrors: number;
  errorsByType: Record<string, number>;
  recentErrors: Array<{
    timestamp: number;
    type: 'query' | 'mutation';
    error: string;
    queryKey?: string;
  }>;
}

function useErrorMonitoring(): ErrorMetrics {
  const [metrics, setMetrics] = useState<ErrorMetrics>({
    totalErrors: 0,
    queryErrors: 0,
    mutationErrors: 0,
    errorsByType: {},
    recentErrors: []
  });

  const trackError = useCallback((
    error: Error,
    type: 'query' | 'mutation',
    queryKey?: string
  ) => {
    const errorType = error.name || 'Unknown';
    
    setMetrics(prev => ({
      totalErrors: prev.totalErrors + 1,
      queryErrors: prev.queryErrors + (type === 'query' ? 1 : 0),
      mutationErrors: prev.mutationErrors + (type === 'mutation' ? 1 : 0),
      errorsByType: {
        ...prev.errorsByType,
        [errorType]: (prev.errorsByType[errorType] || 0) + 1
      },
      recentErrors: [
        {
          timestamp: Date.now(),
          type,
          error: error.message,
          queryKey
        },
        ...prev.recentErrors.slice(0, 9) // Keep last 10 errors
      ]
    }));

    // Send to analytics
    analytics.track('react_query_error', {
      type,
      errorType,
      message: error.message,
      queryKey,
      timestamp: Date.now()
    });
  }, []);

  // Set up global error tracking
  useEffect(() => {
    const queryClient = useQueryClient();
    
    // Override global error handlers to include tracking
    const originalQueryOnError = queryClient.getDefaultOptions().queries?.onError;
    const originalMutationOnError = queryClient.getDefaultOptions().mutations?.onError;

    queryClient.setDefaultOptions({
      queries: {
        ...queryClient.getDefaultOptions().queries,
        onError: (error, query) => {
          trackError(error as Error, 'query', JSON.stringify(query.queryKey));
          originalQueryOnError?.(error, query);
        }
      },
      mutations: {
        ...queryClient.getDefaultOptions().mutations,
        onError: (error, variables, context, mutation) => {
          trackError(error as Error, 'mutation', JSON.stringify(mutation.options.mutationKey));
          originalMutationOnError?.(error, variables, context, mutation);
        }
      }
    });
  }, [trackError]);

  return metrics;
}

function ErrorMetricsPanel() {
  const metrics = useErrorMonitoring();

  return (
    <div className="error-metrics">
      <h3>Error Metrics</h3>
      <div className="metrics-grid">
        <div className="metric">
          <span className="label">Total Errors:</span>
          <span className="value">{metrics.totalErrors}</span>
        </div>
        <div className="metric">
          <span className="label">Query Errors:</span>
          <span className="value">{metrics.queryErrors}</span>
        </div>
        <div className="metric">
          <span className="label">Mutation Errors:</span>
          <span className="value">{metrics.mutationErrors}</span>
        </div>
      </div>

      <h4>Error Types</h4>
      <ul>
        {Object.entries(metrics.errorsByType).map(([type, count]) => (
          <li key={type}>
            {type}: {count}
          </li>
        ))}
      </ul>

      <h4>Recent Errors</h4>
      <ul>
        {metrics.recentErrors.map((error, index) => (
          <li key={index} className="recent-error">
            <span className="timestamp">
              {new Date(error.timestamp).toLocaleTimeString()}
            </span>
            <span className="type">[{error.type}]</span>
            <span className="message">{error.error}</span>
          </li>
        ))}
      </ul>
    </div>
  );
}

Install with Tessl CLI

npx tessl i tessl/npm-react-query

docs

context.md

error-handling.md

index.md

infinite-queries.md

mutations.md

parallel-queries.md

queries.md

ssr.md

status.md

tile.json