Skip to content

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

CodeMeaningRecommended Action
400Bad RequestShow validation errors inline
401UnauthorizedRedirect to login (handled automatically)
403ForbiddenShow "Access denied" message
404Not FoundNavigate back (detail routes) or show empty state (lists)
409ConflictShow specific conflict message (e.g., "Already exists")
422Validation ErrorShow validation errors inline
500Server ErrorShow 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 isApiError type 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 →