# Handle request errors

Use this approach when your application needs different handling for an HTTP error response, a failed connection, or a timeout. [`ky`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky#default) treats non-2xx responses as errors by default and retries eligible failures before rejecting the request.

The samples run in a browser or a Node.js project with Fetch support. Install Ky in your project:

```bash
npm install ky
```

## 1. Distinguish the failure and read its data

An [`HTTPError`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky-httperror#httperror) gives you `response`, `request`, normalized `options`, and pre-parsed `data`. Use [`isHTTPError`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky#ishttperror), [`isNetworkError`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky#isnetworkerror), and [`isTimeoutError`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky#istimeouterror) to narrow the caught value before accessing error-specific properties.

- [`NetworkError`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky#networkerror) represents a network failure, such as DNS failure, connection refusal, or being offline. Read `request.url` and the original error in `cause`; a fetch-phase network failure has no HTTP response.
- [`TimeoutError`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky#timeouterror) identifies an exceeded timeout and gives you the affected `request`.
- [`isKyError`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky#iskyerror) catches other failures in Ky's HTTP lifecycle. Keep a separate fallback for errors outside that lifecycle.

This request targets a nonexistent GitHub API endpoint. If the service returns a non-2xx response, the sample logs the HTTP status and parsed error body; if it returns a successful JSON response, the sample logs the response data instead. `retry: 0` disables retries so the example handles one attempt.

```ts
import ky, {isHTTPError, isNetworkError, isTimeoutError, isKyError} from 'ky';

void (async () => {
	try {
		const data = await ky.get('https://api.github.com/ky-error-example-not-found', {
			retry: 0,
			timeout: 5000,
		}).json();
		console.log('Response data:', data);
	} catch (error) {
		if (isHTTPError(error)) {
			console.error('HTTP status:', error.response.status);
			console.error('Content type:', error.response.headers.get('content-type'));
			console.error('Error body:', error.data);
		} else if (isNetworkError(error)) {
			console.error('Network failure:', error.request.url, error.cause);
		} else if (isTimeoutError(error)) {
			console.error('Timeout:', error.request.url);
		} else if (isKyError(error)) {
			console.error('Ky failure:', error.message);
		} else {
			console.error('Other failure:', error);
		}
	}
})();
```

Ky parses JSON error bodies based on `Content-Type`, using `parseJson` when supplied or `JSON.parse` otherwise. Other content types produce text. `data` can be `undefined` when the body is empty, unreadable, too large, fails parsing, or exceeds the error-data read/parse timeout. Check its shape before reading fields.

Use the caught error's pre-parsed body for recovery decisions; see [Request lifecycle](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/request-lifecycle#returning-a-response-versus-consuming-its-body) for body consumption and response metadata.

### Response-size failures require the next release

Ky 2.1.0 does not include `maxResponseSize`, [`ResponseSizeError`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky#responsesizeerror), or [`isResponseSizeError`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky#isresponsesizeerror). These APIs require the next release; do not import them in a 2.1.0 project.

With that release, `maxResponseSize` limits consumed response-stream bytes after decompression, independently of `Content-Length`. Exceeding it throws `ResponseSizeError` without automatic retries. Use `isResponseSizeError()` to distinguish it, then read `error.maxResponseSize` for the configured byte limit and `error.request` for the request. This is a body-size limit, not an HTTP status failure or a total-memory limit.

## 2. Treat expected HTTP errors as normal responses

Set `throwHttpErrors: false` when checking resource availability and expecting an error response. The call returns the response, leaving you to inspect `ok` or `status` and consume its body.

```ts
import ky from 'ky';

void (async () => {
	try {
		const response = await ky.get('https://api.github.com/ky-error-example-not-found', {
			throwHttpErrors: false,
		});

		if (response.status === 404) {
			console.log('The resource is unavailable.');
		} else if (!response.ok) {
			console.error('Unexpected HTTP status:', response.status);
		} else {
			console.log('Response body:', await response.text());
		}
	} catch (error) {
		console.error('The request failed without a normal HTTP response:', error);
	}
})();
```

**Disabling HTTP errors also disables status-based automatic retries.** Ky considers those error responses successful. Keep `throwHttpErrors` enabled when relying on status-based retries; disable it only when error responses belong to normal application flow. Network failures and timeouts can still reject the call.

For selective handling, pass a function such as `status => status !== 404`. Ky then returns 404 responses normally but throws for other non-2xx statuses. Prefer the boolean form unless you need that distinction.

## 3. Customize the error before it reaches your catch block

Use `hooks.beforeError` to modify an error right before Ky throws it. The hook receives `request`, normalized `options`, `error`, and `retryCount`, and returns an `Error` or a promise of one. `retryCount` is `0` for the initial attempt and increments with each retry.

The example follows the error body's `message` field, when it is a string, and adds the HTTP status. Returning the same error preserves its HTTP-specific properties for the catch block. If the body lacks that field, the original message remains unchanged.

```ts
import ky, {isHTTPError} from 'ky';

const api = ky.extend({
	hooks: {
		beforeError: [
			({error}) => {
				if (
					isHTTPError(error)
					&& typeof error.data === 'object'
					&& error.data !== null
					&& 'message' in error.data
					&& typeof error.data.message === 'string'
				) {
					error.message = `${error.data.message} (${error.response.status})`;
				}

				return error;
			},
		],
	},
});

void (async () => {
	try {
		await api.get('https://api.github.com/ky-error-example-not-found').json();
	} catch (error) {
		if (isHTTPError(error)) {
			console.error(error.message);
		} else {
			console.error(error);
		}
	}
})();
```

`error.data` is populated before `beforeError` runs. Use the shortcut form `await ky(url).json()` to send body-read network and timeout failures through this hook as well. Errors from reading an already returned response are outside the request lifecycle.

## Options that matter

| Option | Type | Default | What it does |
| --- | --- | --- | --- |
| `throwHttpErrors` | `boolean \| ((status: number) => boolean)` | `true` | Controls whether non-2xx responses throw an HTTP error. |
| `hooks.beforeError` | Array of functions returning `Error \| Promise<Error>` | `[]` | Modifies or replaces the error before it is thrown. |
| `timeout` | `number \| false` | `10000` | Sets the per-attempt timeout in milliseconds; shortcuts also use it as a separate body-read timeout. |
| `totalTimeout` | `number \| false` | `false` | Bounds the entire operation, including retries and delays. It does not bound `beforeError` hooks. |

## Handle schema rejection separately

When using `.json(schema)`, handle [`SchemaValidationError`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky#schemavalidationerror) explicitly with `error instanceof SchemaValidationError` and read its `issues`. It does not extend [`KyError`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky#kyerror) and `isKyError()` does not match it: the request succeeded, but your schema rejected the data. See [Send and validate JSON](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/send-and-validate-json) for the validation sample.

## Related

- [Retry failed requests](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/retry-failed-requests) — configure which failures get another attempt.
- [Request lifecycle](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/request-lifecycle) — choose the hook for each stage.
- [Cancel requests](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/cancel-requests) — cancel with an abort signal.
