# How Ky works

Ky is a Fetch-based HTTP client with a shorter request interface, reusable defaults, lifecycle hooks, configurable retries, and separate response and error types.

## Instances hold defaults

The [`KyInstance`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky#kyinstance) type describes the callable client and its method shortcuts. Call `ky.create()` for an independent client. Call `ky.extend()` when a client should inherit a parent’s defaults and add or override values. Use [`replaceOption`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky#replaceoption) when an inherited option must be replaced rather than merged.

```ts title="clients.ts"
import ky, {replaceOption} from 'ky';

const api = ky.create({
	baseUrl: 'https://api.example.com/',
	headers: {'x-client': 'admin'},
});

const users = api.extend({
	baseUrl: 'https://api.example.com/users/',
});

const isolated = ky.create({
	headers: replaceOption({'x-client': 'reports'}),
});

const main = async (): Promise<void> => {
	const response = await users.get('42');
	console.log(response.url);
	console.log(isolated);
};

void main();
```

`extend()` deep-merges options by default: hooks append, headers merge, and search parameters accumulate. `create()` does not inherit the parent’s defaults. The URL printed by the sample is the user client’s URL, and the request uses the inherited header.

## Hooks form the lifecycle

Hooks run at defined points around the request:

1. `init` can synchronously modify mutable options before Ky constructs the request.
2. `beforeRequest` can modify or replace the outgoing request, or return a response to avoid the HTTP request.
3. `beforeRetry` runs only after Ky has selected a retry.
4. `afterResponse` can modify a response or request another attempt.
5. `beforeError` can modify the error before it reaches the caller.

```ts title="lifecycle.ts"
import ky from 'ky';

const api = ky.extend({
	hooks: {
		init: [options => {
			options.searchParams = {source: 'dashboard'};
		}],
		beforeRequest: [({request}) => {
			request.headers.set('Authorization', 'Bearer <token>');
		}],
		afterResponse: [({response}) => {
			return response;
		}],
		beforeError: [({error}) => {
			error.message = `Dashboard request failed: ${error.message}`;
			return error;
		}],
	},
});

const main = async (): Promise<void> => {
	const response = await api.get('https://api.example.com/users');
	console.log(response.status);
};

void main();
```

The request carries the `source` search parameter and authorization header. The response remains available to the caller, while an error receives the added message. Keep authentication and request shaping in hooks rather than duplicating them at every call site.

## Retries are part of the lifecycle

Retry decisions use the configured retry options together with the request method, retry limit, status codes, network errors, timeouts, and server timing headers. `shouldRetry` can override the default retry checks after the method and limit checks pass. `beforeRetry` changes the request only after that decision. An `afterResponse` hook can return `ky.retry()` to force an attempt based on response content; that attempt still respects the retry limit and is visible in `beforeRetry` as a [`ForceRetryError`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky#forceretryerror), which [`isForceRetryError`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky#isforceretryerror) can identify.

```ts title="retry.ts"
import ky, {isForceRetryError} from 'ky';

const api = ky.extend({
	retry: {
		limit: 2,
		retryOnTimeout: true,
	},
	hooks: {
		afterResponse: [async ({response}) => {
			const data = await response.clone().json<{temporary?: boolean}>();
			if (data.temporary === true) {
				return ky.retry();
			}
		}],
		beforeRetry: [({error, retryCount}) => {
			if (isForceRetryError(error)) {
				console.log(`Forced retry #${retryCount}`);
			}
		}],
	},
});

const main = async (): Promise<void> => {
	const response = await api.get('https://api.example.com/status');
	console.log(response.status);
};

void main();
```

The sample logs the returned status. The `clone()` keeps the response available for later processing while the hook reads its body.

## Response data and failures stay distinct

Ky separates an HTTP failure from a successful response whose data fails your schema. [`KyError`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky#kyerror) is the base for Ky lifecycle errors, including [`NetworkError`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky#networkerror), [`TimeoutError`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky#timeouterror), [`HTTPError`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky-httperror#httperror), and [`ForceRetryError`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky#forceretryerror). Use [`isKyError`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky#iskyerror), [`isHTTPError`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky#ishttperror), [`isNetworkError`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky#isnetworkerror), and [`isTimeoutError`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky#istimeouterror) to narrow caught values. `SchemaValidationError` is separate: the request succeeded, but the Standard Schema validator rejected the body, so `isKyError()` does not match it.

```ts title="errors.ts"
import ky, {
	isHTTPError,
	isKyError,
	isNetworkError,
	isTimeoutError,
} from 'ky';

const main = async (): Promise<void> => {
	try {
		await ky.get('https://api.example.com/users/1').json();
	} catch (error) {
		if (isHTTPError(error)) {
			console.log(error.response.status, error.data);
		} else if (isNetworkError(error)) {
			console.log(error.request.url);
		} else if (isTimeoutError(error)) {
			console.log(error.request.url);
		} else if (isKyError(error)) {
			console.log(error.message);
		} else {
			throw error;
		}
	}
};

void main();
```

An [`HTTPError`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky-httperror#httperror) contains the failed response, request, normalized options, and pre-parsed `data`. Ky consumes the response body while populating `data`, so use `error.data` rather than calling a body method on `error.response`; see [Error handling and validation](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/error-handling-and-validation). Network failures have no response and use `NetworkError` instead.

For schema validation, pass a Standard Schema-compatible validator to `.json(schema)`. A failed validation throws [`SchemaValidationError`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky#schemavalidationerror) with its `issues`; it does not turn a successful HTTP response into an HTTP error. Use [JSON requests and responses](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/json-requests-and-responses) for the validation pattern.

These boundaries are the model to carry into the rest of the API: a `KyResponse` represents the received response, hooks control lifecycle points, retry settings control additional attempts, and error classes identify whether the failure came from HTTP, transport, timing, retry control, or data validation. Continue with [Requests and responses](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/requests-and-responses), [Instances and defaults](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/instances-and-defaults), and [Ky’s error model](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky-error-model).
