# Endpoint Error Policy

[Endpoint.errorPolicy](https://dataclient.io/rest/api/Endpoint.md#errorpolicy) controls cache behavior upon a fetch rejection.
It uses the rejection error to determine whether it should be treated as 'soft' or 'hard' error.

### Soft

Soft errors will continue showing valid data if it exists. However, if no previous data is in the store,
it will reject with `error`. In this case [useSuspense()](https://dataclient.io/docs/api/useSuspense.md) throws the
error to be caught by the nearest [ErrorBoundary](https://dataclient.io/docs/api/ErrorBoundary.md) or [AsyncBoundary](https://dataclient.io/docs/api/AsyncBoundary.md)

### Hard

Hard errors always reject with `error` - even when data has previously made available.

'hard' | `undefined` can both be used to indicate this state.

A soft error keeps showing the last data. Hard errors (and soft ones with nothing to show) are left uncaught, so the preview shows them with **Reset preview** to start over.

```ts title="api/lastUpdated"
import { Entity, RestEndpoint } from '@data-client/rest';
import { Temporal } from 'temporal-polyfill';

export class TimedEntity extends Entity {
  id = '';
  updatedAt = Temporal.Instant.fromEpochMilliseconds(0);

  static schema = {
    updatedAt: Temporal.Instant.from,
  };
}

export const lastUpdated = new RestEndpoint({
  path: '/api/currentTime/:id',
  schema: TimedEntity,
  errorPolicy: error =>
    error.status >= 500 ? ('soft' as const) : ('hard' as const),
});
```

```tsx title="TimePage"
import { useSuspense } from '@data-client/react';
import { lastUpdated } from './api/lastUpdated';

export default function TimePage({ id }) {
  const { updatedAt } = useSuspense(lastUpdated, { id });
  return (
    <div>
      API time:{' '}
      <time>
        {updatedAt.toLocaleString('en-US', { timeStyle: 'long' })}
      </time>
    </div>
  );
}
```

```tsx title="ShowTime"
import { useController } from '@data-client/react';
import { Suspense } from 'react';
import { lastUpdated } from './api/lastUpdated';
import TimePage from './TimePage';

function ShowTime() {
  const ctrl = useController();
  // stores a rejected fetch, as if the server answered with `status`
  const fail = (status: number, invalidate = false) => {
    if (invalidate) ctrl.invalidate(lastUpdated, { id: '1' });
    ctrl.setError(
      lastUpdated,
      { id: '1' },
      Object.assign(new Error(`fake ${status} error`), { status }),
    );
  };
  return (
    <div>
      <Suspense fallback={<Loading />}>
        <TimePage id="1" />
      </Suspense>
      <div>
        <button onClick={() => fail(500)}>Fail Soft</button>
        <button onClick={() => fail(400)}>Fail Hard</button>
        <button onClick={() => fail(500, true)}>Invalidate Soft</button>
        <button onClick={() => fail(400, true)}>Invalidate Hard</button>
      </div>
    </div>
  );
}

render(<ShowTime />);
```

### Policy for RestEndpoint

Since `500`s indicate a failure of the server, we want to use stale data
if it exists. On the other hand, something like a `4xx` indicates 'user error', which
means the error indicates something about application flow - like if a record is deleted, resulting
in `404`. Keeping the record around would be inaccurate.

Since this is the typical behavior for REST APIs, this is the default policy in [@data-client/rest](https://www.npmjs.com/package/@data-client/rest)

```ts
errorPolicy(error) {
  return error.status >= 500 ? 'soft' : undefined;
}
```

`undefined` is another way of specifying a [hard error](#hard)
