# Error handling and validation

Use [`ky`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky) to separate HTTP failures, transport failures, timeouts, response-size limits, and response-schema failures. This page covers the request lifecycle, HTTP error data, bounded response reads, and Standard Schema validation.

## When to use this

Use this pattern when a request needs different handling for a non-2xx response, a missing connection, a timeout, a forced retry, or a response that does not match the shape your code expects. A non-2xx response with `throwHttpErrors` enabled throws [`HTTPError`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky-httperror); a schema rejection throws [`SchemaValidationError`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky) after the request succeeds.

## Handle lifecycle failures in one place

Put a `beforeError` hook on an instance when several requests need the same classification or message handling. The hook receives the current request, normalized options, error, and retry count, and returns the `Error` that Ky throws.

For the error taxonomy and the type guards [`isKyError`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky), [`isHTTPError`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky), [`isNetworkError`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky), [`isTimeoutError`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky), and [`isForceRetryError`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky), see [Requests and responses](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/requests-and-responses) and [How Ky works](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/how-ky-works). This page applies that model in a shared `beforeError` hook.

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

const api = ky.extend({
	hooks: {
		beforeError: [({error, request, retryCount}) => {
			if (isHTTPError(error)) {
				const data = error.data;
				if (typeof data === 'object' && data !== null && 'message' in data) {
					error.message = `${String(data.message)} (${error.response.status})`;
				}
			} else if (isNetworkError(error)) {
				console.error(`No response from ${error.request.url}`);
			} else if (isTimeoutError(error)) {
				console.error(`Timed out: ${error.request.url}`);
			} else if (isForceRetryError(error)) {
				console.error(`Retry stopped: ${error.code ?? 'unknown reason'}`);
			} else if (isKyError(error)) {
				console.error(`Ky request failed on attempt ${retryCount + 1}: ${request.url}`);
			}

			return error;
		}],
	},
});

const loadUser = async (): Promise<void> => {
	try {
		await api.get('https://api.example.com/user').json();
	} catch (error) {
		console.error(error);
	}
};

void loadUser();
```

The call returns parsed JSON on success. On failure, the hook classifies the error before the `catch` block receives it. A network failure has no response; use its request instead. `isKyError()` covers Ky's HTTP lifecycle errors, but not `SchemaValidationError`.

## Read an HTTP error body

[Requests and responses](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/requests-and-responses) covers the standard `HTTPError` fields. In an application error handler, use the pre-parsed payload to turn a structured server error into a user-facing message:

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

const readAccount = async (): Promise<void> => {
	try {
		await ky.get('https://api.example.com/account').json();
	} catch (error) {
		if (isHTTPError(error)) {
			const data = error.data;
			const message = typeof data === 'object' && data !== null && 'message' in data
				? String(data.message)
				: 'The account request failed';
			console.error(`${message} (HTTP ${error.response.status})`);
		} else {
			throw error;
		}
	}
};

void readAccount();
```

For how Ky parses and consumes an HTTP error body, see [Requests and responses](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/requests-and-responses). The sample uses the resulting `data` value to choose a message while retaining `response.status` for context.

## Limit response size

`maxResponseSize` limits bytes read from the decompressed response stream. Set it to a non-negative safe integer; `0` permits only an empty body. Exceeding the limit cancels the stream and raises `ResponseSizeError` without an automatic retry. The limit bounds body bytes, not all memory used by parsing, buffering, or concurrent requests.

`maxResponseSize` and [`isResponseSizeError`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky) are in the repository's next release, not in Ky 2.1.0, the current npm release. Use the following with a build that contains that API:

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

const readArchive = async (): Promise<void> => {
	try {
		await ky('https://api.example.com/archive', {
			maxResponseSize: 1024 * 1024,
		}).json();
	} catch (error) {
		if (isResponseSizeError(error)) {
			console.error(`Response exceeded ${error.maxResponseSize} bytes`);
		} else {
			throw error;
		}
	}
};

void readArchive();
```

The request can resolve before a later body read exceeds the limit when you use `await ky(url)` without a body shortcut; consume the body to observe the limit.

## Validate the JSON response

For the `.json(schema)` flow and the distinction between [`SchemaValidationError`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky) and Ky lifecycle errors, see [How Ky works](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/how-ky-works). This page adds a reporting pattern that turns each validation issue into a path-and-message entry.

```ts
import ky, {SchemaValidationError} from 'ky';
import {z} from 'zod';

const userSchema = z.object({
	name: z.string(),
});

const readUser = async (): Promise<void> => {
	try {
		const user = await ky('https://api.example.com/user').json(userSchema);
		console.log(`Loaded ${user.name}`);
	} catch (error) {
		if (error instanceof SchemaValidationError) {
			for (const issue of error.issues) {
				const path = issue.path?.map(part => typeof part === 'object' ? String(part.key) : String(part)).join('.') ?? '<root>';
				console.error(`${path}: ${issue.message}`);
			}
		} else {
			console.error('Request failed', error);
		}
	}
};

void readUser();
```

The success path receives a value with `name: string`; a rejected response produces one log entry per issue, including its property path. For the boundary between schema validation and Ky lifecycle errors, see [How Ky works](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/how-ky-works).

## Options that affect failures

| Option | Type | Default | What it does |
| --- | --- | --- | --- |
| `throwHttpErrors` | `boolean` | `true` | Throws an `HTTPError` for a non-2xx response after redirects. |
| `timeout` | `number \| false` | `10000` | Sets the per-attempt timeout for receiving a response and, for shortcut methods, reading the body. |
| `totalTimeout` | `number \| false` | `false` | Sets an overall limit for the operation, including retries and delays. |
| `maxResponseSize` | `number` | `Infinity` | Limits decompressed response-body bytes; available in the next release, not Ky 2.1.0. |

`beforeError` runs after an error exists and is not bounded by `totalTimeout`. Use `shouldRetry` to change retry decisions; `beforeRetry` runs only after Ky has selected a retry.

## Pitfall

For the response-body consumption rule, see [Requests and responses](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/requests-and-responses). In this page's handler, keep the pre-parsed `error.data` for the message and use `error.response` only for metadata such as the status.

## Related

- [Ky error model](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky-error-model)
- [Ky](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky)
- [HTTPError](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky-httperror)
