# Ky errors and validation

Ky separates errors in its HTTP lifecycle from errors raised after a successful response fails schema validation.

## How the errors differ

[`KyError`](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/ky#kyerror) is the base class for errors Ky raises during its HTTP lifecycle. A non-2xx response raises [`HTTPError`](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/ky-httperror#httperror) when `throwHttpErrors` is enabled; it includes the response, request, normalized options, and pre-parsed response data. A failed connection raises [`NetworkError`](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/ky#networkerror), which includes the request and original error as `cause`. A request timeout raises [`TimeoutError`](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/ky#timeouterror), which includes the request. A forced retry initiated by an `afterResponse` hook is represented by [`ForceRetryError`](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/ky#forceretryerror).

By contrast, [`SchemaValidationError`](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/ky#schemavalidationerror) means Ky received a successful response, parsed its JSON, and the supplied Standard Schema validator rejected that data. It extends `Error`, not `KyError`, and exposes the validator's issues.

The lifecycle error family also includes [`ResponseSizeError`](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/ky#responsesizeerror), raised when a response body exceeds `maxResponseSize`; it carries the configured byte limit in `maxResponseSize`. Use [`isResponseSizeError`](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/ky#isresponsesizeerror) to narrow a caught value to that error type. These APIs are unreleased and require the next Ky release; they are not available in the current npm release, Ky 2.1.0.

In that release, `ResponseSizeError` is a `KyError`, and `isResponseSizeError(error)` identifies it.

```mermaid
flowchart TD
    A["Ky request"] --> B{"HTTP lifecycle"}
    B -->|"non-2xx, throwHttpErrors enabled"| C["HTTPError"]
    B -->|"connection failure"| D["NetworkError"]
    B -->|"request timeout"| E["TimeoutError"]
    B -->|"response body exceeds maxResponseSize"| J["ResponseSizeError"]
    B -->|"forced retry from afterResponse"| F["ForceRetryError"]
    B -->|"successful response"| G["Parse and validate JSON"]
    G -->|"schema rejects data"| H["SchemaValidationError"]
    G -->|"schema accepts data"| I["Validated data"]
```

## Identify an error at the call site

Use Ky's type guards to narrow unknown caught values to lifecycle errors. Check the schema error separately with `instanceof`; the catch block below handles it before the Ky lifecycle guards. The sample calls [`ky`](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/ky#default) and uses Zod as a Standard Schema-compatible validator. Install both packages with `npm install ky zod`, and point the request at an endpoint that returns a JSON user object.

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

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

async function loadUser(): Promise<void> {
	try {
		const user = await ky.get('https://api.example.com/users/1').json(userSchema);
		console.log('Validated user:', user);
	} catch (error) {
		if (error instanceof SchemaValidationError) {
			console.error('Response data did not match the schema:', error.issues);
		} else if (isHTTPError(error)) {
			console.error('HTTP status:', error.response.status);
			console.error('Error response body:', error.data);
		} else if (isNetworkError(error)) {
			console.error('Network failure for:', error.request.url);
		} else if (isTimeoutError(error)) {
			console.error('Timed out request:', error.request.url);
		} else if (isKyError(error)) {
			console.error('Other Ky lifecycle error:', error.message);
		} else {
			console.error('Other error:', error);
		}
	}
}

void loadUser();
```

If the response contains a user with a string `name`, the call returns and logs the validated user. A non-2xx response enters the `HTTPError` branch, a connection failure or timeout enters its matching branch, and a successful response with invalid data enters the schema-error branch. The guards narrow `error` so each branch can read that error type's properties.

Ky detects network errors with runtime-specific heuristics, so an unrecognized runtime may not wrap a connection failure in `NetworkError`.

When Ky fills `HTTPError.data`, it consumes the response body. Read the parsed error body from `error.data`; calling `error.response.json()` or another body method afterward does not work. The response remains available for status and headers, and `data` can be `undefined` if the body is empty, unreadable, or cannot be parsed.

For the general lifecycle guards and hook timing, see [How Ky works](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/how-ky-works). For the additional forced-retry case, [`isForceRetryError`](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/ky#isforceretryerror) identifies the `ForceRetryError` signal in `beforeRetry` and `beforeError` hooks.

## Related

- [How Ky works](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/how-ky-works)
- [The request lifecycle](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/the-request-lifecycle)
- [Validate JSON responses](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/validate-json-responses)
- [Retry failed requests](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/retry-failed-requests)
