Error Handling
Effective error handling is crucial for building robust applications. @kitbag/query provides multiple layers of error handling for both queries and mutations.
Basic Error Handling
Query Errors
vue
<template>
<div>
<div v-if="user.executing">Loading...</div>
<div v-else-if="user.error" class="error">
<h3>Failed to load user</h3>
<p>{{ user.error.message }}</p>
<button @click="user.execute()">Retry</button>
</div>
<div v-else-if="user.data">
<!-- User data -->
</div>
</div>
</template>
<script setup lang="ts">
const user = useQuery(userQuery, {
params: [userId],
onError: (error) => {
console.error('User query failed:', error)
}
})
</script>
Mutation Errors
vue
<template>
<form @submit.prevent="handleSubmit">
<input v-model="userData.name" />
<input v-model="userData.email" />
<button type="submit" :disabled="createUser.executing">
Create User
</button>
<div v-if="createUser.error" class="error">
{{ createUser.error.message }}
</div>
</form>
</template>
<script setup lang="ts">
const createUser = useMutation(createUserMutation, {
onError: ({ error, payload }) => {
console.error('Failed to create user:', error)
showErrorToast('Failed to create user')
}
})
</script>
Error Types
Network Errors
Handle network connectivity issues:
ts
const userQuery = query('user', async (id: number) => {
try {
const response = await fetch(`/api/users/${id}`)
return response.json()
} catch (error) {
// Network error
if (!navigator.onLine) {
throw new Error('You appear to be offline. Please check your connection.')
}
// Timeout or other network issue
throw new Error('Network error occurred. Please try again.')
}
})
HTTP Errors
Handle different HTTP status codes:
ts
const userQuery = query('user', async (id: number) => {
const response = await fetch(`/api/users/${id}`)
if (!response.ok) {
switch (response.status) {
case 404:
throw new Error('User not found')
case 403:
throw new Error('Access denied')
case 500:
throw new Error('Server error. Please try again later.')
default:
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
}
return response.json()
})
Validation Errors
Handle validation errors from the API:
ts
const createUserMutation = mutation(async (userData: CreateUserData) => {
const response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(userData)
})
if (!response.ok) {
const errorData = await response.json()
if (response.status === 422) {
// Validation error
const error = new Error('Validation failed')
error.status = 422
error.data = errorData
throw error
}
throw new Error(errorData.message || 'Failed to create user')
}
return response.json()
})
Error Boundaries
Global Error Handling
Set up global error handling for all queries and mutations:
ts
// queryClient.ts
import { createQueryClient } from '@kitbag/query'
export const { query, useQuery, mutation, useMutation } = createQueryClient({
defaultQueryOptions: {
onError: (error) => {
// Log all query errors
console.error('Query error:', error)
// Handle specific error types globally
if (error.status === 401) {
// Redirect to login
router.push('/login')
} else if (error.status >= 500) {
// Show generic server error message
showToast('Server error. Please try again later.', 'error')
}
}
},
defaultMutationOptions: {
onError: ({ error }) => {
console.error('Mutation error:', error)
// Global mutation error handling
if (error.status === 401) {
router.push('/login')
}
}
}
})
Component Error Boundaries
Create reusable error boundary components:
vue
<!-- ErrorBoundary.vue -->
<template>
<div class="error-boundary">
<div v-if="hasError" class="error-container">
<h2>Something went wrong</h2>
<p>{{ errorMessage }}</p>
<button @click="retry">Try Again</button>
<button @click="reset">Reset</button>
</div>
<slot v-else />
</div>
</template>
<script setup lang="ts">
import { ref, provide, onErrorCaptured } from 'vue'
const hasError = ref(false)
const errorMessage = ref('')
const retry = ref(() => {})
const reset = () => {
hasError.value = false
errorMessage.value = ''
}
// Provide error boundary functions to children
provide('errorBoundary', {
captureError: (error: Error, retryFn?: () => void) => {
hasError.value = true
errorMessage.value = error.message
retry.value = retryFn || reset
}
})
onErrorCaptured((error) => {
hasError.value = true
errorMessage.value = error.message
return false // Prevent error from propagating
})
</script>
Usage:
vue
<template>
<ErrorBoundary>
<UserProfile :user-id="userId" />
</ErrorBoundary>
</template>
Retry Logic
Automatic Retries
Configure automatic retries for transient errors:
ts
const userQuery = query('user', fetchUser, {
retry: 3, // Retry up to 3 times
retryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 30000) // Exponential backoff
})
// Custom retry logic
const importantQuery = query('important', fetchImportantData, {
retry: (failureCount, error) => {
// Don't retry on client errors (4xx)
if (error.status >= 400 && error.status < 500) {
return false
}
// Retry up to 5 times for server errors
return failureCount < 5
}
})
Manual Retries
Provide manual retry functionality:
vue
<template>
<div>
<div v-if="user.error" class="error">
<p>{{ user.error.message }}</p>
<button @click="retry" :disabled="retrying">
{{ retrying ? 'Retrying...' : 'Retry' }}
</button>
</div>
</div>
</template>
<script setup lang="ts">
const retrying = ref(false)
const user = useQuery(userQuery, { params: [userId] })
async function retry() {
retrying.value = true
try {
await user.execute()
} finally {
retrying.value = false
}
}
</script>
Error Recovery
Fallback Data
Provide fallback data when queries fail:
vue
<script setup lang="ts">
const user = useQuery(userQuery, {
params: [userId],
placeholder: {
id: userId,
name: 'Unknown User',
email: 'unknown@example.com'
},
onError: (error) => {
// Keep placeholder data on error
console.warn('Using fallback user data due to error:', error)
}
})
// Always have data available
const displayUser = computed(() => user.data.value || user.placeholder)
</script>
Partial Data Recovery
Handle scenarios where some data loads successfully:
vue
<script setup lang="ts">
const userProfile = useQuery(userProfileQuery, { params: [userId] })
const userPosts = useQuery(userPostsQuery, { params: [userId] })
const userSettings = useQuery(userSettingsQuery, { params: [userId] })
// Show what we can, even if some queries fail
const canShowProfile = computed(() => !userProfile.error)
const canShowPosts = computed(() => !userPosts.error)
const canShowSettings = computed(() => !userSettings.error)
</script>
<template>
<div>
<div v-if="canShowProfile">
<!-- User profile section -->
</div>
<div v-else class="error">
Failed to load profile
</div>
<div v-if="canShowPosts">
<!-- User posts section -->
</div>
<div v-else class="error">
Failed to load posts
</div>
</div>
</template>
User Experience
Error Messages
Provide user-friendly error messages:
ts
function getErrorMessage(error: any): string {
// Network errors
if (!navigator.onLine) {
return 'You appear to be offline. Please check your connection.'
}
// HTTP status codes
switch (error.status) {
case 400:
return 'Invalid request. Please check your input.'
case 401:
return 'Please log in to continue.'
case 403:
return 'You do not have permission to access this resource.'
case 404:
return 'The requested resource was not found.'
case 429:
return 'Too many requests. Please wait a moment and try again.'
case 500:
return 'Server error. Please try again later.'
default:
return error.message || 'An unexpected error occurred.'
}
}
const user = useQuery(userQuery, {
params: [userId],
onError: (error) => {
const message = getErrorMessage(error)
showToast(message, 'error')
}
})
Loading and Error States
Provide clear feedback during error recovery:
vue
<template>
<div>
<!-- Loading state -->
<div v-if="user.executing && !user.data" class="loading">
Loading user profile...
</div>
<!-- Error state with retry -->
<div v-else-if="user.error" class="error">
<h3>Unable to load profile</h3>
<p>{{ getErrorMessage(user.error) }}</p>
<div class="error-actions">
<button @click="user.execute()" :disabled="user.executing">
{{ user.executing ? 'Retrying...' : 'Try Again' }}
</button>
<button @click="goBack">Go Back</button>
</div>
</div>
<!-- Success state -->
<div v-else-if="user.data">
<!-- Profile content -->
</div>
</div>
</template>
Offline Support
Handle offline scenarios gracefully:
vue
<script setup lang="ts">
import { useOnline } from '@vueuse/core'
const isOnline = useOnline()
const user = useQuery(userQuery, {
params: [userId],
enabled: isOnline, // Only execute when online
onError: (error) => {
if (!isOnline.value) {
showToast('You are offline. Data will sync when connection is restored.', 'info')
}
}
})
// Retry when coming back online
watch(isOnline, (online) => {
if (online && user.error.value) {
user.execute()
}
})
</script>
<template>
<div>
<div v-if="!isOnline" class="offline-banner">
You are currently offline
</div>
<!-- Rest of component -->
</div>
</template>
Error Monitoring
Logging and Analytics
Set up error monitoring:
ts
// errorTracking.ts
interface ErrorContext {
userId?: string
operation: string
params?: any
timestamp: Date
}
function trackError(error: Error, context: ErrorContext) {
// Send to error tracking service
if (window.Sentry) {
window.Sentry.captureException(error, {
tags: {
operation: context.operation
},
user: {
id: context.userId
},
extra: {
params: context.params,
timestamp: context.timestamp
}
})
}
// Send to analytics
if (window.gtag) {
window.gtag('event', 'exception', {
description: error.message,
fatal: false
})
}
}
// Use in queries
const user = useQuery(userQuery, {
params: [userId],
onError: (error) => {
trackError(error, {
userId: currentUser.id,
operation: 'fetchUser',
params: [userId],
timestamp: new Date()
})
}
})
Error Reporting
Create an error reporting component:
vue
<!-- ErrorReporter.vue -->
<template>
<div class="error-reporter">
<h3>Something went wrong</h3>
<p>{{ error.message }}</p>
<details>
<summary>Technical Details</summary>
<pre>{{ errorDetails }}</pre>
</details>
<div class="actions">
<button @click="retry">Try Again</button>
<button @click="reportError">Report Issue</button>
</div>
</div>
</template>
<script setup lang="ts">
interface Props {
error: Error
context?: any
onRetry?: () => void
}
const props = defineProps<Props>()
const errorDetails = computed(() => ({
message: props.error.message,
stack: props.error.stack,
context: props.context,
timestamp: new Date().toISOString(),
userAgent: navigator.userAgent,
url: window.location.href
}))
function retry() {
props.onRetry?.()
}
function reportError() {
// Send error report
fetch('/api/error-reports', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(errorDetails.value)
})
showToast('Error reported. Thank you!', 'success')
}
</script>
Best Practices
1. Provide Meaningful Error Messages
ts
// ✅ Good - User-friendly messages
const getErrorMessage = (error) => {
if (error.status === 404) return 'User not found'
if (error.status === 403) return 'Access denied'
return 'Something went wrong. Please try again.'
}
// ❌ Bad - Technical error messages
const error = 'HTTP 500: Internal Server Error'
2. Handle Errors at the Right Level
ts
// ✅ Handle specific errors locally
const user = useQuery(userQuery, {
onError: (error) => {
if (error.status === 404) {
router.push('/users')
}
}
})
// ✅ Handle common errors globally
const queryClient = createQueryClient({
defaultQueryOptions: {
onError: (error) => {
if (error.status === 401) {
logout()
}
}
}
})
3. Provide Recovery Options
vue
<!-- ✅ Always provide a way to recover -->
<div v-if="error" class="error">
<p>{{ error.message }}</p>
<button @click="retry">Try Again</button>
<button @click="goBack">Go Back</button>
</div>
4. Use Appropriate Retry Logic
ts
// ✅ Retry transient errors only
const query = query('data', fetcher, {
retry: (count, error) => {
// Don't retry client errors
if (error.status >= 400 && error.status < 500) {
return false
}
// Retry server errors up to 3 times
return count < 3
}
})
5. Monitor and Track Errors
ts
// ✅ Track errors for monitoring
const user = useQuery(userQuery, {
onError: (error) => {
trackError(error, { operation: 'fetchUser', userId })
}
})
Next Steps
- Loading States - Managing loading UX
- Background Updates - Handling background data refresh
- Optimistic Updates - Improving perceived performance