Skip to content

useApiError ​

The one place that decides what a failed request looks like. A toast per status — except 422, which comes back as data for the form.

Import ​

ts
import { useApiError } from '@shared/composables/useApiError'

Signature ​

ts
function useApiError(): {
  handleError: (error: unknown) => ValidationError[] | void
}

ValidationError is { field: string, message: string }.

What each status does ​

StatusWhat the reader seesReturn
400The server's own message, or "Invalid request".void
401"Session expired — redirecting to login"void
403"You do not have permission for this action"void
404"Resource not found"void
422Nothing. The form is about to light up per field.ValidationError[]
429A warning toast: "Too many requests — please slow down"void
500, 502, 503, 504"Server error — please try again later"void
Timed out (ECONNABORTED)"Request timed out — please try again"void
No response at all"Network error — check your connection"void
Anything elseThe server's message, or "Something went wrong".void
Not an axios error"Something went wrong", plus a console.error.void

Every message is a translation key resolved at the moment it is shown.

Example ​

vue
<script setup lang="ts">
import { useApiError } from '@shared/composables/useApiError'
import { api } from '@shared/lib/axios'
import { useForm } from 'vee-validate'

const { handleError } = useApiError()
const { values, setErrors, handleSubmit } = useForm()

const submit = handleSubmit(async () => {
  try {
    await api.post('/invoices', values)
  }
  catch (e) {
    const fieldErrors = handleError(e)
    if (fieldErrors)
      setErrors(Object.fromEntries(fieldErrors.map(err => [err.field, err.message])))
  }
})
</script>

<template>
  <form @submit="submit">
    <slot />
  </form>
</template>

Notes ​

  • The if (fieldErrors) branch is the whole contract. Only 422 returns an array. Everything else has already been shown to the reader, so there is nothing left for the caller to do.
  • 401 is handled before this. The axios interceptor refreshes the token and redirects. Reaching this branch means the refresh itself failed, and the toast covers the tick before the redirect lands.
  • A non-axios throwable — a programmer error, an aborted request, a network drop outside axios — gets the generic toast and a console.error, so it is visible in development without a blank failure in production.
  • handleError never rethrows. If the caller needs the error to propagate, it must rethrow itself, as useCrud does.