Appearance
Error Handling
Common patterns for handling API errors and user-facing error messages.
404 Navigation Pattern
Critical Pattern: When fetching a resource by ID on a detail route (/resource/id/*), if you get a 404, navigate back to the parent list route.
Why?
404 errors on detail routes typically happen when:
- User switches organizations (resource exists in org A, not org B)
- User returns to a bookmarked link with a different account
- Resource was deleted while user was away
- Invalid ID from URL manipulation
Navigating back to the list route provides a better UX than showing an error page.
Implementation
vue
<script setup>
const route = useRoute('hr-orgId-employees-module-id')
const employeeId = computed(() => route.params.id)
const module = computed(() => route.params.module)
// Fetch employee data
const { data, error } = useOrgApi(`/api/organizations/employees/${employeeId.value}`)
// Watch for 404 errors and navigate back to list
watch(error, (newError) => {
if (newError && newError.statusCode === 404) {
navigateTo({ name: 'hr-orgId-employees-module', params: { module: module.value } })
}
})
</script>Pattern Rules
When to Use ✅
- Detail routes:
/employees/[id],/policies/[id], etc. - Navigate back to parent list route
- Use route names for navigation (dynamic params persist)
When NOT to Use ⚠️
- List routes (show empty state instead)
- User-initiated actions (show toast notification)
- Form submissions (show inline validation)
Error Messages
With useApi/useOrgApi
Errors are automatically typed. Access the error directly:
vue
<script setup>
const { data, error, status } = useOrgApi('/api/organizations/employees')
const errorMessage = computed(() => {
if (!error.value) return null
// error.value is fully typed with statusCode, message, etc.
if (error.value.statusCode === 404) return 'Employee not found'
if (error.value.statusCode === 403) return 'Access denied'
return error.value.message || 'An error occurred'
})
</script>
<template>
<div v-if="error" class="text-destructive">
{{ errorMessage }}
</div>
</template>See useApi documentation for more patterns.
With $apiFetch
Use isApiError type guard for type safety:
vue
<script setup>
const { $apiFetch } = useNuxtApp()
const handleSubmit = async (formData: ApiBody<'/api/organizations/employees', 'post'>) => {
try {
const result = await $apiFetch('/api/organizations/employees', {
method: 'POST',
body: formData
})
toast.success('Employee created!')
return result
} catch (err) {
// Type guard for API errors
if (isApiError<'/api/organizations/employees', 'post'>(err)) {
// err is now fully typed
if (err.statusCode === 409) {
toast.error('Employee already exists')
} else if (err.statusCode === 422) {
toast.error('Invalid employee data')
} else {
toast.error(err.message || 'Failed to create employee')
}
} else {
// Network or other errors
toast.error('Network error')
}
}
}
</script>See $apiFetch documentation for more patterns.
Toast Notifications
Use the global toast API for user-facing error messages:
vue
<script setup>
const { toast } = useToast()
const handleDelete = async () => {
const confirmed = await useConfirm()('Delete employee?', {
variant: 'destructive'
})
if (!confirmed) return
try {
await $apiFetch('/api/organizations/employees/123', { method: 'DELETE' })
toast.success('Employee deleted')
navigateTo('/hr/employees')
} catch (err) {
if (isApiError(err)) {
toast.error(err.message || 'Failed to delete employee')
}
}
}
</script>Toast Variants
ts
toast.success('Operation completed')
toast.error('Something went wrong')
toast.warning('Please review your changes')
toast.info('New feature available')Common Error Status Codes
| Code | Meaning | Recommended Action |
|---|---|---|
400 | Bad Request | Show validation errors inline |
401 | Unauthorized | Redirect to login (handled automatically) |
403 | Forbidden | Show "Access denied" message |
404 | Not Found | Navigate back (detail routes) or show empty state (lists) |
409 | Conflict | Show specific conflict message (e.g., "Already exists") |
422 | Validation Error | Show validation errors inline |
500 | Server Error | Show generic error, log for debugging |
Validation Errors
For form validation errors, show them inline:
vue
<script setup>
const { handleSubmit, errors } = useForm({
validationSchema: toTypedSchema(employeeSchema)
})
const onSubmit = handleSubmit(async (values) => {
try {
await $apiFetch('/api/organizations/employees', {
method: 'POST',
body: values
})
toast.success('Employee created')
} catch (err) {
if (isApiError(err) && err.statusCode === 422) {
// Show validation error in toast
toast.error('Please check the form for errors')
}
}
})
</script>
<template>
<form @submit="onSubmit">
<UiFormField name="email">
<UiFormLabel>Email</UiFormLabel>
<UiFormControl>
<UiInput />
</UiFormControl>
<UiFormMessage /> <!-- Shows validation error -->
</UiFormField>
</form>
</template>See Forms & Validation → for complete form error handling.
Best Practices
Do's ✅
- Use 404 navigation pattern on detail routes
- Show toast notifications for user actions
- Use
isApiErrortype guard with$apiFetch - Provide specific error messages based on status code
- Log unexpected errors for debugging
Don'ts ❌
- Don't show error pages for 404s on detail routes (navigate back instead)
- Don't ignore errors silently
- Don't show technical error messages to users
- Don't forget to handle network errors
Quick Reference
vue
<script setup>
// 404 Navigation Pattern (detail routes)
const { data, error } = useOrgApi(`/api/resource/${id}`)
watch(error, (err) => {
if (err?.statusCode === 404) {
navigateTo({ name: 'resource-list' })
}
})
// Error handling with $apiFetch
try {
await $apiFetch('/api/resource', { method: 'POST', body })
toast.success('Success!')
} catch (err) {
if (isApiError<'/api/resource', 'post'>(err)) {
if (err.statusCode === 409) {
toast.error('Already exists')
} else {
toast.error(err.message)
}
}
}
</script>Related: API Calls → | Forms & Validation → | UI Components →