# Errors, timeouts and retries (https://docs.salla.dev/twilight/react/api/errors-timeouts-and-retries)



Read this when a request fails, or before you decide what a loader should do when the API is down.

## What failure looks like [#what-failure-looks-like]

The client rejects the promise; it never returns a half-result. The error is one of:

| Error          | When                                      | What you can read                                       |
| -------------- | ----------------------------------------- | ------------------------------------------------------- |
| `HTTPError`    | The server answered with a non-2xx status | `error.response.status`, `error.data` (the parsed body) |
| `TimeoutError` | No answer within the timeout              | `error.request`                                         |
| `NetworkError` | Offline, DNS or connection failure        | `error.request`, `error.cause`                          |
| `AbortError`   | You cancelled with an `AbortSignal`       | a standard `DOMException`                               |

These come from [ky](https://github.com/sindresorhus/ky), the library under the client. A 404 or 422 is an `HTTPError`, not a normal return:

```ts
import { api } from '@salla.sa/twilight-theme-engine/api/client';

try {
  await api.get('products/0/details').json();
} catch (error) {
  const status = (error as { response?: { status?: number } }).response?.status;
  if (status === 404) { /* no such product */ }
  else throw error;
}
```

## Timeout and retries [#timeout-and-retries]

* **Timeout:** 8 seconds per attempt. Change it for one call with `api.get(path, { timeout: 3000 })`. `store.settings` uses 5 seconds and the `apps` calls 2.5 seconds.
* **Retries:** `GET`, `PUT`, `HEAD`, `DELETE` and `OPTIONS` are retried up to **2 times** on a network error or on status 408, 413, 429, 500, 502, 503 or 504, waiting 0.3 s then 0.6 s (or the server's `Retry-After`). `POST` and `PATCH` are never retried, so a failed checkout step is not submitted twice.
* A timeout is **not** retried. Other 4xx errors such as 401 or 404 are not retried either.

To change this for one call, set `retry`: `api.get(path, { retry: 0 })`. The full numbers are in the [client reference](https://docs.salla.dev/twilight/react/api/client#timeouts-and-retries).

## In a route loader [#in-a-route-loader]

An error thrown in a loader goes to the route's error boundary. Two helpers from `@salla.sa/twilight-theme-engine/providers` shape that:

* `orThrow(promise)` turns a 404 or 410 into the not-found page and rethrows every other error (it logs the error with `console.debug`). Modules expose it as `product.findOrThrow(id)`, `cart.getOrThrow(id)` and similar.
* `orUnauthorized(promise)` turns a 401 or 403 into the unauthorized page and rethrows everything else untouched.

The two combine: `order.findOrThrow(id)` is `orThrow(orUnauthorized(order.find(id)))`, so a 401 or 403 becomes the unauthorized page and a 404 or 410 the not-found page.

```ts
import { orUnauthorized } from '@salla.sa/twilight-theme-engine/providers';
import { product } from '@salla.sa/twilight-theme-engine/api/product';
import { order } from '@salla.sa/twilight-theme-engine/api/order';

const found = await product.findOrThrow(id);        // 404 or 410: not-found page
const mine = await orUnauthorized(order.find(id));  // 401 or 403: unauthorized page
```

<Callout type="warn">
  Only 404 and 410 become the not-found page. A 500, a timeout or a 401 still reaches the route's error boundary, so use `orThrow` for pages that mean nothing without the data, and catch the error yourself where you want a softer failure.
</Callout>

## In a component [#in-a-component]

With a query, failure is state, not an exception. The engine's query client retries a failed query once, so `isError` only becomes true after that retry:

```tsx
import { useQuery } from '@tanstack/react-query';
import { order } from '@salla.sa/twilight-theme-engine/api/order';

export function Orders() {
  const { data, error, isPending, refetch } = useQuery(order.queries.list());

  if (isPending) return <p>Loading</p>;
  if (error) return <button onClick={() => refetch()}>Try again</button>;
  return <p>{data.data.length} orders</p>;
}
```

For a one-off action, such as a button that calls an endpoint, wrap it with [useAsyncFn](https://docs.salla.dev/twilight/react/hooks/use-async-fn). It gives you `loading` and `error` and does not throw.

## Cancelling a request [#cancelling-a-request]

Pass an `AbortSignal`. A query function receives one, and modules that accept `signal` forward it:

```ts
queryFn: ({ signal }) => api.get('products', { searchParams, signal }).json(),
```

TanStack Query aborts the request when the component unmounts or the key changes. The abort rejects the call, so do not show it to the customer as a failure.

## Next [#next]

* [Write your own endpoint](https://docs.salla.dev/twilight/react/api/write-your-own-endpoint): a module whose errors behave like the built-in ones.
* [Loading data](https://docs.salla.dev/twilight/react/guide/loading-data): loaders and error boundaries.
* [Modules](https://docs.salla.dev/twilight/react/api/modules): each page says which methods swallow errors and which throw.
