# How Ky works

Ky is an HTTP client built on Fetch that adds body shortcuts, error handling, retries, composable defaults, and lifecycle hooks.

These five ideas explain what you pass to [`ky`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky#default), what you get back, and where to customize a request.

## 1. Fetch is the foundation

Pass a string, `URL`, or `Request`, with standard Fetch options plus Ky's additional options. Await the call to get a response with standard properties such as `status` and `headers`.

```ts
import ky from 'ky';

const url = new URL('https://api.example.com/users');
async function main() {
	const response = await ky(url, {
		credentials: 'omit',
		headers: {'X-Client': 'dashboard'},
	});

	console.log(response.status, response.headers.get('content-type'));
}

main().catch(console.error);
```

This sends a GET request and logs the status and content type your service returns. The samples use absolute URLs so they also work outside the browser; point them at your service.

Keep using standard web APIs: pass `FormData` or a `ReadableStream` as `body`, and an `AbortController`'s signal as `signal`. See [files and streams](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/transfer-files-and-streams) and [cancellation](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/cancel-requests) for those tasks.

## 2. Request and consume a response in one expression

Call a body shortcut directly on the request promise, without awaiting the response first. Ky sets an appropriate `Accept` header for the shortcut unless you already supplied one.

The `json` option handles outgoing data: it serializes the value and sets `Content-Type: application/json` unless your `headers` option specifies another content type. The `.json()` shortcut handles incoming data and returns `unknown` by default.

```ts
import ky from 'ky';

async function main() {
	const result = await ky.post('https://api.example.com/users', {
		json: {name: 'Ada'},
	}).json();

	console.log(result);
}

main().catch(console.error);
```

This sends JSON and gives you the parsed response body. Inspect your service's result rather than assuming it echoes the submitted object.

Use `.json<User>()` when you have an application type named `User`. A type parameter describes the result to TypeScript; it does not validate the response. For runtime validation, pass a Standard Schema compatible validator, such as a Zod schema, to `.json(schema)`. A rejected value throws [`SchemaValidationError`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky#schemavalidationerror). See [send and validate JSON](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/send-and-validate-json) for a complete typed and validated example.

Other body shortcuts include `.text()`, `.formData()`, `.arrayBuffer()`, and `.blob()`. The `.bytes()` shortcut exists only when the runtime supports `Response.prototype.bytes()`.

## 3. Errors and retries are a policy, not manual loops

By default, a non-2xx HTTP response becomes an [`HTTPError`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky-httperror#httperror). Network failures and timeouts have distinct types: [`NetworkError`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky#networkerror) and [`TimeoutError`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky#timeouterror).

Automatic retries are bounded by `retry.limit` and restricted by method and failure type. The default limit is two retries; POST is not among the default retriable methods. `retry.shouldRetry` can override the default failure checks, but only after the limit and method checks pass. Timeouts do not trigger retries by default.

Use [`isHTTPError`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky#ishttperror) to narrow a caught error and inspect its status and pre-parsed error data.

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

async function main() {
	try {
		const result = await ky.get('https://api.example.com/users', {
			retry: {limit: 2},
			timeout: 5000,
			totalTimeout: 15_000,
		}).json();
		console.log(result);
	} catch (error) {
		if (isHTTPError(error)) {
			console.error(error.response.status, error.data);
		} else {
			throw error;
		}
	}
}

main().catch(console.error);
```

This request allows up to two retries for eligible failures; it does not guarantee a retry or success.

`timeout` gives each attempt five seconds to get a response, and body shortcuts use it as a separate body-read timeout. `totalTimeout` bounds the overall operation, including retries and delays, to fifteen seconds. `beforeError` hooks are outside that overall budget.

See [retry failed requests](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/retry-failed-requests) for eligibility and delay controls, and [handle request errors](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/handle-request-errors) for error-body handling, `throwHttpErrors`, and schema-error classification.

## 4. Compose defaults into specialized instances

Use `ky.create()` for complete new defaults. Use `.extend()` to inherit and merge a parent's defaults: hooks append, headers merge, and search parameters accumulate. [`replaceOption`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky#replaceoption) replaces an inherited value instead of merging it.

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

const api = ky.create({
	baseUrl: 'https://api.example.com/',
	headers: {'X-Client': 'dashboard'},
	searchParams: {locale: 'en'},
});

const usersApi = api.extend({
	headers: {'X-Feature': 'users'},
	searchParams: replaceOption({locale: 'fr'}),
});

async function main() {
	const result = await usersApi.get('users').json();
	console.log(result);
}

main().catch(console.error);
```

The request goes to `https://api.example.com/users?locale=fr` with both custom headers. The child inherits the base URL and client header, adds a feature header, and replaces the parent's search parameters. Creating the child does not change the parent.

See [instances and defaults](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/instances-and-defaults) for composition and [resolve request URLs](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/resolve-request-urls) for URL resolution.

## 5. Hooks belong to specific lifecycle stages

Hooks customize a stage rather than acting as interchangeable interceptors. For the ordinary network request path, the stages relate as follows:

```mermaid
flowchart TD
	A["init: change options"] --> B["Construct Request"]
	B --> C["beforeRequest: initial request"]
	C --> D["Fetch attempt"]
	D --> E["afterResponse: read or replace response"]
	D --> F["Network failure or timeout"]
	E --> G["Check HTTP status"]
	G --> H["Return response or consume body"]
	G --> I["Retry decision"]
	F --> I
	E -->|"ky.retry()"| I
	I -->|"Retry confirmed"| J["Delay and beforeRetry"]
	J --> D
	I -->|"No retry"| K["beforeError, then throw"]
```

- `init` synchronously changes options before request construction.
- `beforeRequest` changes the initial outgoing request and runs once, with `retryCount: 0`.
- `beforeRetry` changes a request only after a retry is confirmed.
- `afterResponse` receives a response clone to read, or can return a replacement `Response`.
- `beforeError` receives the error before it is thrown and must return an `Error`.

```ts
import ky from 'ky';

const api = ky.extend({
	hooks: {
		beforeRequest: [({request}) => {
			request.headers.set('X-Client', 'dashboard');
		}],
		afterResponse: [({response, retryCount}) => {
			console.log('Response status:', response.status, 'Retries:', retryCount);
		}],
	},
});

async function main() {
	const result = await api.get('https://api.example.com/users').json();
	console.log(result);
}

main().catch(console.error);
```

This adds a header to the outgoing request and logs each response that reaches `afterResponse`; it leaves the response unchanged for `.json()` to consume.

Both `beforeRequest` and `beforeRetry` can return a `Request` to replace the outgoing request, or a `Response` to bypass the corresponding network attempt. An `afterResponse` hook can return `ky.retry()` to retry based on response content—even a successful HTTP status. That forced retry still respects `retry.limit`, but bypasses the method check and `shouldRetry`.

See [request lifecycle](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/request-lifecycle) for hook return values and [authenticate requests](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/authenticate-requests) for authentication hooks.

## Next steps

Start with the [quick start](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/quick-start) to make your first request, or follow [send and validate JSON](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/send-and-validate-json) to give your response data a runtime contract.
