Error Handling
Error types
| Error | Package | Thrown when |
|---|---|---|
ValidationError | @routar/core | Request or response validation fails |
HttpError | @routar/core | Server returns a non-2xx status (all executors — fetch, Axios, ky) |
TimeoutError | @routar/core | Request exceeds the createFetchExecutor timeout |
All transports normalize HTTP failures to HttpError. The Axios and ky executors catch their native errors (AxiosError, ky HTTPError) and re-throw an HttpError with status, statusText, and body populated from the response. The original transport error is preserved on err.cause, so you can still reach AxiosError-specific fields when you need them.
Handling errors
import { TimeoutError, ValidationError } from '@routar/core'
import { HttpError } from '@routar/core'
try {
await todoApi.create({ body: { title: '' } })
} catch (err) {
if (err instanceof ValidationError) {
// request or response schema failed
console.log(err.message)
console.log(err.cause) // original Zod/Valibot error
}
if (err instanceof HttpError) {
// fetch returned non-2xx
console.log(err.status) // e.g. 422
console.log(err.statusText) // e.g. 'Unprocessable Entity'
console.log(err.body) // parsed JSON payload, or null
console.log(err.url) // full request URL, e.g. 'https://api.example.com/todos'
console.log(err.method) // HTTP method, e.g. 'POST'
console.log(err.cause) // original transport error (AxiosError, ky HTTPError), or undefined
}
if (err instanceof TimeoutError) {
console.log(err.ms) // configured timeout in ms
}
}Transport-agnostic HTTP errors
Because every executor normalizes HTTP failures to HttpError, a single instanceof HttpError check handles fetch, Axios, and ky alike — no need to branch on isAxiosError or ky’s HTTPError:
import { HttpError } from '@routar/core'
try {
await todoApi.getDetail({ path: { id: 1 } })
} catch (err) {
if (err instanceof HttpError) {
// any executor — non-2xx response
console.log(err.status, err.body)
}
}If you need transport-specific fields (e.g. an AxiosError’s config or code), reach them through err.cause:
import { HttpError } from '@routar/core'
import { isAxiosError } from 'axios'
try {
await todoApi.getDetail({ path: { id: 1 } })
} catch (err) {
if (err instanceof HttpError && isAxiosError(err.cause)) {
console.log(err.cause.code) // e.g. 'ERR_BAD_REQUEST'
console.log(err.cause.config) // original request config
}
}onError plugins see the same normalized HttpError, so plugin code is transport-agnostic too. (Network failures and cancellations — errors with no response — are re-thrown unchanged, since there is no HTTP status to normalize.)
Retry only on server errors
retry is an option on createFetchExecutor:
import { createFetchExecutor, HttpError } from '@routar/core'
createFetchExecutor('https://api.example.com', {
retry: {
count: 3,
shouldRetry: (err) => {
if (err instanceof HttpError) return err.status >= 500
return true // retry on network errors
},
},
})