Skip to Content
GuidesError Handling

Error Handling

Error types

ErrorPackageThrown when
ValidationError@routar/coreRequest or response validation fails
HttpError@routar/coreServer returns a non-2xx status (all executors — fetch, Axios, ky)
TimeoutError@routar/coreRequest 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 }, }, })
Last updated on