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
}