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
| Status | What the reader sees | Return |
|---|---|---|
400 | The 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 |
422 | Nothing. The form is about to light up per field. | ValidationError[] |
429 | A 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 else | The 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. handleErrornever rethrows. If the caller needs the error to propagate, it must rethrow itself, asuseCruddoes.
Related
useCrud— calls this on every operation.- Connect your API — the 422 shape a backend has to send.
- Toasts — what the messages are drawn with.