00
:
00
:
00
:
00
Corso SEO AI - Usa SEOEMAIL al checkout per il 30% di sconto

Gestione Errori e Retry

Configurazione Retry

TanStack Query ritenta automaticamente le query fallite. Di default, ritenta 3 volte con un delay esponenziale.

Opzioni di Retry

const { data } = useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  retry: 3,                // numero di tentativi (default: 3)
  retryDelay: (attemptIndex) => {
    // Delay esponenziale: 1s, 2s, 4s, 8s... fino a 30s
    return Math.min(1000 * 2 ** attemptIndex, 30000)
  },
  retryOnMount: true,      // ritenta al mount se la query e' in errore
})

Retry Condizionale

Puoi decidere se ritentare in base al tipo di errore:

const { data } = useQuery({
  queryKey: ['user', userId],
  queryFn: () => fetchUser(userId),
  retry: (failureCount, error: any) => {
    // Non ritentare per errori 404 (risorsa non esiste)
    if (error.status === 404) return false
    // Non ritentare per errori 401 (non autenticato)
    if (error.status === 401) return false
    // Per altri errori, ritenta fino a 3 volte
    return failureCount < 3
  },
})

Configurazione Globale

Imposta il retry a livello globale per tutte le query:

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      retry: (failureCount, error: any) => {
        if (error.status >= 400 && error.status < 500) return false
        return failureCount < 3
      },
      retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
    },
    mutations: {
      retry: 0, // nessun retry per le mutations di default
    },
  },
})

Gestione Errori Locale

Errori a Livello di Componente

Il modo piu’ semplice e’ gestire l’errore direttamente nel componente:

function UserProfile({ userId }: { userId: string }) {
  const { data, error, isError, refetch } = useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchUser(userId),
  })

  if (isError) {
    return (
      <div className="error-container">
        <p>Impossibile caricare il profilo: {error.message}</p>
        <button onClick={() => refetch()}>Riprova</button>
      </div>
    )
  }

  return <div>{data?.name}</div>
}

Callback onError nelle Mutations

Per le mutations, puoi usare i callback per gestire gli errori:

const mutation = useMutation({
  mutationFn: createTodo,
  onError: (error, variables, context) => {
    console.error('Creazione todo fallita:', error.message)
    console.error('Dati inviati:', variables)
  },
  onSuccess: (data) => {
    toast.success('Todo creato con successo!')
  },
})

Error Boundaries

Per delegare la gestione degli errori a React Error Boundaries, usa l’opzione throwOnError:

import { useQuery } from '@tanstack/react-query'
import { ErrorBoundary } from 'react-error-boundary'

function TodoListContent() {
  // Lancia l'errore al boundary piu' vicino
  const { data } = useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
    throwOnError: true,
  })

  return (
    <ul>
      {data.map((todo) => (
        <li key={todo.id}>{todo.title}</li>
      ))}
    </ul>
  )
}

function TodoPage() {
  return (
    <ErrorBoundary
      fallbackRender={({ error, resetErrorBoundary }) => (
        <div>
          <p>Errore: {error.message}</p>
          <button onClick={resetErrorBoundary}>Riprova</button>
        </div>
      )}
      onReset={() => {
        // Opzionale: invalida le query quando l'utente clicca "Riprova"
      }}
    >
      <TodoListContent />
    </ErrorBoundary>
  )
}

throwOnError Condizionale

Puoi lanciare l’errore al boundary solo per certi tipi di errore:

const { data } = useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  throwOnError: (error: any) => {
    // Lancia al boundary solo per errori server (5xx)
    return error.status >= 500
  },
})

Gestione Errori Globale

Puoi configurare callback globali sul QueryClient per gestire errori in modo centralizzato:

const queryClient = new QueryClient({
  queryCache: new QueryCache({
    onError: (error, query) => {
      // Mostra un toast solo se la query aveva gia' dati in cache
      // (significa che e' un background refetch fallito)
      if (query.state.data !== undefined) {
        toast.error(`Aggiornamento fallito: ${error.message}`)
      }
    },
  }),
  mutationCache: new MutationCache({
    onError: (error) => {
      toast.error(`Operazione fallita: ${error.message}`)
    },
  }),
})

Toast di Errore con React Hot Toast

Esempio completo con notifiche:

import { QueryClient, QueryCache, MutationCache } from '@tanstack/react-query'
import toast from 'react-hot-toast'

const queryClient = new QueryClient({
  queryCache: new QueryCache({
    onError: (error: any, query) => {
      if (error.status === 401) {
        // Redirect al login per errori di autenticazione
        window.location.href = '/login'
        return
      }

      if (query.state.data !== undefined) {
        toast.error(`Impossibile aggiornare i dati: ${error.message}`)
      }
    },
  }),
  mutationCache: new MutationCache({
    onError: (error: any) => {
      if (error.status === 403) {
        toast.error('Non hai i permessi per questa operazione')
      } else if (error.status >= 500) {
        toast.error('Errore del server. Riprova piu\' tardi.')
      } else {
        toast.error(error.message || 'Operazione fallita')
      }
    },
  }),
  defaultOptions: {
    queries: {
      retry: (failureCount, error: any) => {
        if (error.status === 401 || error.status === 403) return false
        return failureCount < 3
      },
    },
  },
})

Errore Custom Tipizzato

Crea un tipo errore personalizzato per avere piu’ controllo:

class ApiError extends Error {
  constructor(
    public status: number,
    public statusText: string,
    message: string,
  ) {
    super(message)
    this.name = 'ApiError'
  }
}

async function fetchWithError<T>(url: string): Promise<T> {
  const res = await fetch(url)
  if (!res.ok) {
    throw new ApiError(res.status, res.statusText, `Errore ${res.status}: ${res.statusText}`)
  }
  return res.json()
}

// Uso con tipo errore esplicito
const { error } = useQuery<User, ApiError>({
  queryKey: ['user', userId],
  queryFn: () => fetchWithError<User>(`/api/users/${userId}`),
})

if (error) {
  console.log(error.status) // TypeScript conosce il tipo ApiError
}