# ky · GPT-5.6 Luna # JavaScript quick start Install Ky and make a JSON request with the default client. Ky is a Fetch-based HTTP client with a simpler request and response interface. Use the [`ky`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky#default) default export with Fetch-compatible inputs and options; its response provides direct body shortcuts such as `.json()`. ## Prerequisites - npm. - A JavaScript or TypeScript project that can use ES modules. Ky targets modern browsers, Node.js, Bun, and Deno. ## 1. Install Ky Run this command in your project: ```bash npm install ky ``` The command adds Ky to the project. ## Handle an HTTP failure Non-2xx responses throw an [`HTTPError`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky-httperror#httperror) when HTTP errors are enabled. Use the [`isHTTPError`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky#ishttperror) type guard when the caller needs to inspect the status: ```ts title="src/load-user.ts" import ky, {isHTTPError} from 'ky'; const main = async (): Promise => { try { const user = await ky.get('https://jsonplaceholder.typicode.com/users/1').json(); console.log(user); } catch (error) { if (isHTTPError(error)) { console.error(`Request failed with status ${error.response.status}`); } else { throw error; } } }; void main(); ``` The successful path receives parsed JSON. The failure path receives an `HTTPError` with the response status; network failures do not have a response because no response was received. ## What you have now Your project can install Ky, send a request with the default client, and receive parsed JSON through `.json()`. For the request model and response shortcuts, continue to [Requests and responses](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/requests-and-responses). To reuse URL and request defaults, see [Instances and defaults](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/instances-and-defaults). # 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 => { 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 '); }], afterResponse: [({response}) => { return response; }], beforeError: [({error}) => { error.message = `Dashboard request failed: ${error.message}`; return error; }], }, }); const main = async (): Promise => { 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 => { 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 => { 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). # Requests and responses Ky is a Fetch-based HTTP client that builds a request from a Fetch-compatible input and options, then exposes response body shortcuts, typed JSON, and schema validation on the returned promise. ## The request and response flow Pass a string, URL, or Request as the input. Ky applies its options, constructs the Request, performs the fetch, and gives you a response promise whose body methods can be called before awaiting the response object. ```mermaid flowchart LR A["Input + options"] --> B["Ky builds Request"] B --> C["Fetch"] C --> D["ResponsePromise"] D --> E["json(), text(), blob(), formData(), arrayBuffer(), bytes()"] E --> F["typed value or schema-validated value"] C --> G["HTTPError for non-2xx"] E --> H["SchemaValidationError for rejected JSON"] ``` The `input` accepts the [`Input`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky#input) type: a `string`, `URL`, or `Request`. The default [`ky`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky#default) export also provides method shortcuts such as `ky.post()`. A shortcut sets the request method; the other options remain Fetch options or Ky options. When you pass `json`, Ky stringifies the value, uses it as the request body, and sets `Content-Type: application/json` unless your `headers` option supplies a content type. Use `json` instead of manually creating a `body` for JSON data. ```ts import ky from 'ky'; async function main(): Promise { const result = await ky.post('https://api.example.com/users', { json: {name: 'Ada'}, }).json<{id: string; name: string}>(); console.log(result.id, result.name); } main(); ``` This sends a JSON `POST` and returns the parsed response body as the type supplied to `json()`. The promise returned by the request is a [`ResponsePromise`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky-responsepromise#responsepromise), so you do not need to await a separate response before selecting a body method. ## Choose a response body The response promise provides `json()`, `text()`, `formData()`, `arrayBuffer()`, `blob()`, and `bytes()` shortcuts. `bytes()` is available when the runtime supports `Response.prototype.bytes()`. ```ts import ky from 'ky'; async function main(): Promise { const text = await ky.get('https://api.example.com/status').text(); const file = await ky.get('https://api.example.com/report.pdf').blob(); const raw = await ky.get('https://api.example.com/archive.bin').arrayBuffer(); console.log(text, file.size, raw.byteLength); } main(); ``` Each call consumes the response body in the selected representation. If you need response metadata as well, await the promise itself. The resulting [`KyResponse`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky#kyresponse) has the standard `Response` properties and methods plus Ky's typed `json()` method. ```ts import ky from 'ky'; async function main(): Promise { const response = await ky.get('https://api.example.com/status'); console.log(response.status, response.headers.get('content-type')); const body = await response.json<{status: string}>(); console.log(body.status); } main(); ``` Calling `json()` for an empty response throws because there is no JSON body to parse. When an empty body is part of your protocol, configure `parseJson` to provide the required handling rather than assuming that `json()` can parse it. ## Type JSON at the boundary Without a type argument, `json()` returns `unknown`. Give the request or the body method a type argument when your application has a known response shape. ```ts import ky from 'ky'; type User = { id: string; name: string; }; async function main(): Promise { const fromRequestType = await ky('https://api.example.com/users/1').json(); const fromBodyType = await ky('https://api.example.com/users/1').json(); console.log(fromRequestType.name, fromBodyType.id); } main(); ``` These type arguments describe the value TypeScript expects; they do not validate the bytes returned by the server. Use a Standard Schema-compatible validator when the response must be checked at runtime. ## Validate JSON at runtime Pass a Standard Schema validator to `json(schema)`. Ky returns the validator's inferred output when validation succeeds and throws [`SchemaValidationError`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky#schemavalidationerror) when it fails. ```ts import ky, {SchemaValidationError} from 'ky'; import {z} from 'zod'; const userSchema = z.object({ name: z.string(), }); async function main(): Promise { try { const user = await ky('https://api.example.com/users/1').json(userSchema); console.log(user); } catch (error) { if (error instanceof SchemaValidationError) { console.error(error.issues); } else { throw error; } } } main(); ``` Schema validation happens after the request succeeds. `SchemaValidationError` is therefore not a Ky lifecycle error and is not matched by [`isKyError`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky#iskyerror)(). ## Separate HTTP failures from missing responses Ky throws [`HTTPError`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky-httperror#httperror) for a non-2xx response when `throwHttpErrors` is enabled. The error retains the `KyResponse` in `error.response`, the [`KyRequest`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky-kyrequest#kyrequest) in `error.request`, normalized options, and a pre-parsed `error.data` value when Ky can read the error body. Because Ky consumes the body while populating `error.data`, use `error.data` rather than calling `error.response.json()` in this case. For the error classes and type guards, see [How Ky works](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/how-ky-works#response-data-and-failures-stay-distinct). ```ts import ky, {isHTTPError} from 'ky'; async function main(): Promise { try { await ky.get('https://api.example.com/users/1').json<{name: string}>(); } catch (error) { if (isHTTPError(error)) { console.error('Status:', error.response.status); console.error('Error body:', error.data); } else { throw error; } } } main(); ``` The HTTP branch can inspect the status and parsed error data. The network branch does not try to read a response because none was received. Throw an ordinary error when a caller needs a propagated failure from a retry hook; returning `ky.stop` produces an undefined response that cannot be followed by `.json()` or `.text()`. ## Related pages - [How Ky works](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/how-ky-works) - [JSON requests and responses](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/json-requests-and-responses) - [Ky's error model](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky-error-model) - [Error handling and validation](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/error-handling-and-validation) # Instances and defaults Ky instances collect request defaults so you can share a transport policy while keeping different API clients independent. Use [`ky`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky) directly for one-off requests, `ky.create()` for a new set of defaults, and `ky.extend()` when a client should inherit and modify another client. ## How the defaults compose `create()` starts a new instance. It does not inherit defaults from the default instance or from another client. `extend()` starts from its parent, then merges the options you provide. A function form of `extend()` receives the parent options, so a child can derive a value such as a longer URL prefix. ```mermaid flowchart LR A["ky.create(defaults)"] --> B["base client"] B --> C["ky.extend(changes)"] C --> D["child client"] E["request options"] --> F["merged request"] B --> F D --> F ``` The instance exposes method shortcuts such as `get()` and `post()`. Each shortcut returns a response promise with body methods such as `.text()` and `.json()`; the method shortcut supplies the HTTP method while the instance supplies its defaults. ## Create a shared client Pass an [`Options`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky-options) object to `create()`. The following client gives every relative request a base URL, a path prefix, a header, and a request hook. The trailing slash on `baseUrl` keeps page-relative paths under `/api/` when standard URL resolution applies. ```ts import ky from 'ky'; async function main(): Promise { const api = ky.create({ baseUrl: 'https://api.example.com/api/', prefix: 'v1', headers: { accept: 'application/json', }, hooks: { beforeRequest: [({request}) => { request.headers.set('x-client', 'web'); }], }, retry: 2, timeout: 10_000, }); const response = await api.get('users/42'); console.log(response.url); } main(); ``` The request uses the instance's defaults and resolves to a URL under the configured base URL and prefix. The hook runs immediately before the request is sent, and `retry` and `timeout` apply to requests made through this instance unless a request supplies different values. ## Extend a client for a narrower API Call `extend()` on the parent instance when a child shares the parent's policy. The function form lets you read the parent options. This example adds `users` to the existing prefix and adds a child-specific header without rebuilding the base client. ```ts import ky from 'ky'; async function main(): Promise { const api = ky.create({ baseUrl: 'https://api.example.com/api/', prefix: 'v1', headers: { accept: 'application/json', }, }); const usersApi = api.extend(parentOptions => ({ prefix: `${parentOptions.prefix}/users`, headers: { 'x-resource': 'users', }, })); const response = await usersApi.get('42'); console.log(response.url); } main(); ``` `usersApi` keeps the base URL and inherited `accept` header, adds the resource header, and sends `GET /api/v1/users/42`. Calling `api.get('version')` still uses the parent prefix and does not use the child prefix. ## Know what is merged By default, `extend()` deep-merges options. Headers merge, search parameters accumulate, and hook arrays append. This is useful for layering policy, but it is not a replacement operation. | Option | Type | Default | Effect when extended | | --- | --- | --- | --- | | `baseUrl` | `string \| URL` | — | Resolves relative input after `prefix` is applied. An absolute input bypasses it. | | `prefix` | `string \| URL` | — | Joins before URL resolution; a slash at the join boundary is normalized. | | `headers` | header object | — | Merges with parent headers. | | `hooks` | [`Hooks`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky-hooks) | — | Appends hook arrays by default. | | `searchParams` | [`SearchParamsOption`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky) | `''` | Accumulates with the parent's search parameters. | | `retry` | [`RetryOptions`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky-retryoptions) \| number | — | Supplies retry policy for requests from the instance. | | `timeout` | `number \| false` | — | Sets the per-attempt response timeout in milliseconds. | Use [`replaceOption`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky) when a child must replace a merged value rather than add to it. For example, this child runs only its own `beforeRequest` hook: ```ts import ky, {replaceOption} from 'ky'; async function main(): Promise { const api = ky.create({ hooks: { beforeRequest: [() => { console.log('parent hook'); }], }, }); const child = api.extend({ hooks: replaceOption({ beforeRequest: [() => { console.log('child hook'); }], }), }); await child.get('https://example.com'); } main(); ``` The child hook replaces the parent's `beforeRequest` array. The same marker can replace other deep-merged values, including headers, search parameters, context, and signals. ## Choose `baseUrl` or `prefix` Use `baseUrl` in most cases. It follows standard URL resolution: an input beginning with `/` starts at the origin root, so it can override a path in the base URL. Use `prefix` when an origin-relative input such as `/users` must be treated as page-relative and appended to the prefix. The prefix is joined before `baseUrl` resolves the resulting input. For a base URL with a path, include its trailing `/` when you want `users` and `./users` to extend that path rather than replace its last segment. A `Request` input bypasses both `baseUrl` and `prefix`; `searchParams` still applies. ## Related - [`Options`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky-options) — request and instance options. - [`ResponsePromise`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky-responsepromise) — body methods returned by an instance request. - [Shared clients and URL defaults](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/shared-clients-and-url-defaults) — the warning about inherited headers, search parameters, and hooks. # Ky's error model Ky separates failures in the HTTP lifecycle from failures in the data you validate after a response arrives. Use the error type to decide whether to inspect a response, retry a request, report a timeout, or fix the schema. Call the default export [`ky`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky#default) for requests and method shortcuts such as `ky.get()`. ## One hierarchy, one deliberate exception The [`KyError`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky#kyerror) hierarchy and its deliberate [`SchemaValidationError`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky#schemavalidationerror) exception are covered in [How Ky works](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/how-ky-works); this page applies that distinction when choosing an error branch. ```mermaid flowchart TD Request["Ky request"] --> Response{"Response received?"} Response -->|"no"| Network["NetworkError"] Response -->|"yes"| Status{"2xx status?"} Status -->|"no"| HTTP["HTTPError"] Status -->|"yes"| Body{"Body operation succeeds?"} Body -->|"timeout"| Timeout["TimeoutError"] Body -->|"too large"| Size["ResponseSizeError"] Body -->|"JSON schema rejects"| Schema["SchemaValidationError"] Body -->|"yes"| Value["Validated or typed value"] ``` For the broad cross-realm [`isKyError`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky#iskyerror) check and its schema-validation boundary, see [How Ky works](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/how-ky-works). ## HTTP failures An [`HTTPError`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky-httperror#httperror) means Ky received a non-2xx response while `throwHttpErrors` is enabled. Inspect `error.response.status`, `error.response.headers`, and `error.data`. Ky populates `data` before error hooks run and parses JSON responses with `parseJson` when that option is set, or with `JSON.parse` otherwise. For other content types, `data` is text. Ky consumes the response body while populating `error.data`. Do not call `error.response.json()` or another body method afterwards; use `error.data`. The response remains useful for status and headers. See [error handling and validation](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/error-handling-and-validation) for the handling pattern used by this guide. Use [`isHTTPError`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky#ishttperror) to narrow an unknown catch value before reading the response. ```ts import ky, {isHTTPError} from 'ky'; async function loadStatus(): Promise { try { await ky.get('https://api.example.com/status').json(); } catch (error) { if (isHTTPError(error)) { console.error(error.response.status, error.data); return; } throw error; } } void loadStatus(); ``` The call either gives you the parsed body shortcut or enters the HTTP branch with the status and pre-parsed error data. A network failure does not enter this branch because it has no response to expose. ## Network and timeout failures [`NetworkError`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky#networkerror) means the request failed at the network layer, such as through DNS failure, connection refusal, or an offline runtime. It carries the `request`, and the original runtime error is its `cause`. Network errors are automatically retried for retriable methods. A connection that drops while `.json()` or another shortcut reads an already received response is also wrapped as `NetworkError`, but Ky does not retry that body-read failure because the response already arrived. Runtime-specific detection can miss an unfamiliar error shape; use the `shouldRetry` option for that case. [`TimeoutError`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky#timeouterror) means the request timed out and also carries the `request`. `timeout` applies to an attempt, while `totalTimeout` limits the whole operation across attempts and delays. Both are distinct from a server response with a non-2xx status. Use [`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 these two cases. ```ts import ky, {isNetworkError, isTimeoutError} from 'ky'; async function requestWithDiagnostics(): Promise { try { await ky.get('https://api.example.com/report', { timeout: 1_000, totalTimeout: 5_000, }).json(); } catch (error) { if (isTimeoutError(error)) { console.error('Timed out:', error.request.url); return; } if (isNetworkError(error)) { console.error('Network failure:', error.request.url); return; } throw error; } } void requestWithDiagnostics(); ``` The timeout branch reports an attempt or overall time limit. The network branch reports a request that did not produce a usable response. A network failure while a shortcut reads an already received response is not retried; use `shouldRetry` when your runtime needs a custom retry decision. ## Response-size failures [`ResponseSizeError`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky#responsesizeerror) is the shipped error for a response body that exceeds `maxResponseSize`. It carries the request and the configured byte limit. The limit counts bytes from the decompressed response stream as Ky consumes it, and exceeding it does not automatically retry. `ResponseSizeError`, `maxResponseSize`, and [`isResponseSizeError`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky#isresponsesizeerror) are not in the current Ky 2.1.0 npm release. Use them only after installing a release that exports them; do not add this branch to an application pinned to 2.1.0. ## Forced retries Use the forced-retry branch when response content—not a transport failure—requires another attempt. The lifecycle and hook pattern are covered in [How Ky works](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/how-ky-works); this page shows how to identify the resulting [`ForceRetryError`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky#forceretryerror) with [`isForceRetryError`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky#isforceretryerror). ```ts import ky, {isForceRetryError} from 'ky'; const api = ky.extend({ retry: {limit: 1}, hooks: { afterResponse: [() => ky.retry()], beforeRetry: [({error, retryCount}) => { if (isForceRetryError(error)) { console.log(`Forced retry #${retryCount}: ${error.code}`); } }], }, }); async function runForcedRetry(): Promise { try { await api.get('https://api.example.com/catalog'); } catch (error) { if (isForceRetryError(error)) { console.log('Retry limit reached:', error.code); return; } throw error; } } void runForcedRetry(); ``` This differs from an ordinary retry because the response has already arrived: the `beforeRetry` branch can identify the explicit response-driven request, and the configured limit bounds repeated requests. ## Schema-validation failures `.json()` can receive a Standard Schema-compatible validator. [`SchemaValidationError`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky#schemavalidationerror) means Ky received the response and the validator rejected its JSON value. Read its `issues` property. Do not classify it with `isKyError()`; handle it separately from transport, status, retry, and timeout failures. ```ts import ky, {isKyError, SchemaValidationError} from 'ky'; import {z} from 'zod'; const userSchema = z.object({name: z.string()}); async function loadUser(): Promise { try { const user = await ky.get('https://api.example.com/user').json(userSchema); console.log('Validated user:', user.name); } catch (error) { if (error instanceof SchemaValidationError) { console.error('The response arrived, but validation failed:', error.issues); return; } if (isKyError(error)) { console.error('The HTTP lifecycle failed:', error.message); return; } throw error; } } void loadUser(); ``` The success path receives the schema-inferred value, while the validation branch receives the validator's issues. A successful HTTP response does not guarantee a successful schema validation. ## Choosing a check For HTTP, network, and broad lifecycle checks, use the guidance in [Requests and responses](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/requests-and-responses). This page adds the timeout, forced-retry, response-size, and schema-validation distinctions described above. For shared defaults, [`replaceOption`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky#replaceoption) can replace a merged option when you build a derived instance; it does not change the error classification. The default export [`ky`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky#default) provides the request shortcuts and the `retry` control used above. # Send JSON and read responses Use [`ky`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky#default) to put a JSON value in a request and read the response directly as JSON. Choose a TypeScript type when the response shape is trusted, or pass a Standard Schema when the response must be checked at runtime. ## Send a JSON request Pass the value to the `json` option instead of serializing it into `body`. Ky serializes the value and sets `Content-Type: application/json` unless you provide that header yourself. The request shortcut returns a response promise, so you can call `json()` without first awaiting a raw response. ```ts import ky from 'ky'; interface NewUser { name: string; email: string; } interface User extends NewUser { id: string; } const newUser: NewUser = { name: 'Ada Lovelace', email: 'ada@example.com', }; const createUser = async (): Promise => ky .post('https://api.example.com/users', {json: newUser}) .json(); const showCreatedUser = async (): Promise => { const user = await createUser(); console.log(user.id, user.name); }; void showCreatedUser(); ``` The server receives a JSON object with `name` and `email`. The response is parsed as JSON and `user` has the compile-time type `User`; the type parameter does not validate the server's data at runtime. ## Read a typed JSON response `.json()` defaults to `unknown`. Give the request or the body method a type parameter when you want a typed result: ```ts import ky from 'ky'; interface User { id: string; name: string; } const showTypedUsers = async (): Promise => { const fromRequestType = await ky('https://api.example.com/users/1').json(); const fromBodyMethodType = await ky('https://api.example.com/users/1').json(); console.log(fromRequestType.name, fromBodyMethodType.id); }; void showTypedUsers(); ``` Both calls produce a parsed JSON object typed as `User`. The body method is a direct shortcut; it also sets an appropriate `Accept` header for JSON. A non-2xx response rejects instead of producing a successful value. ## Validate the response with a schema Install a Standard Schema-compatible validator such as Zod alongside Ky: ```bash npm install ky zod ``` Pass the schema to `.json(schema)`. Ky returns the validator's inferred output only after validation succeeds. A rejected schema produces [`SchemaValidationError`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky#schemavalidationerror), not a Ky HTTP lifecycle error. ```ts import ky, {SchemaValidationError} from 'ky'; import {z} from 'zod'; const userSchema = z.object({ id: z.string(), name: z.string(), }); const createUser = async (): Promise => { try { const user = await ky.post('https://api.example.com/users', { json: {name: 'Ada Lovelace'}, }).json(userSchema); console.log(user.id, user.name); } catch (error) { if (error instanceof SchemaValidationError) { console.error('The response shape is invalid', error.issues); return; } throw error; } }; const run = async (): Promise => createUser(); void run(); ``` This combines a JSON request with runtime validation of the JSON response: the request sends `name`, and the call returns a value only when the response also contains a string `id` and `name`. ## Handle HTTP failures separately For non-2xx responses and Ky's [`HTTPError`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky-httperror#httperror) and [`isHTTPError`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky#ishttperror) handling, see [Requests and responses](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/requests-and-responses). ```ts import ky, {isHTTPError} from 'ky'; const loadUser = async (): Promise => { try { const user = await ky('https://api.example.com/users/1').json<{name: string}>(); console.log(user.name); } catch (error) { if (isHTTPError(error)) { console.error('Request failed', error.response.status, error.data); return; } throw error; } }; const run = async (): Promise => loadUser(); void run(); ``` `error.response` remains available for status and headers. If no response arrives, the failure is not an `HTTPError`; handle network failures separately when your application needs that distinction. ## Options that matter here | Option | Type | Default | What it does | | --- | --- | --- | --- | | `json` | any value accepted by `JSON.stringify()` | not set | Serializes the value into the request body and sets `Content-Type: application/json` unless your `headers` override it. | | `headers` | Fetch headers | not set | Supplies request headers; an explicit `Content-Type` takes precedence over Ky's JSON header. | | `throwHttpErrors` | boolean | enabled | Controls whether non-2xx responses reject as `HTTPError`. | | `parseJson` | function | `JSON.parse` | Replaces JSON parsing for response bodies, including handling an empty body. | ## Pitfalls - Do not use a TypeScript type parameter as a runtime check. Use `.json(schema)` when the response must be validated. - `.json()` cannot parse an empty body by default. Configure `parseJson` when an endpoint legitimately returns an empty response. - Read failed-response content from `HTTPError.data`, not from `HTTPError.response.json()`. - Keep the request container's URL real in application code. The URLs above are examples; replace them with your API's endpoint. ## Related - [Requests and responses](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/requests-and-responses) - [Ky's error model](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky-error-model) - [Error handling and validation](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/error-handling-and-validation) # Share clients and URL defaults Use a reusable [`ky`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky) instance when several requests share an API origin, path prefix, headers, or request options, then override them per request or replace them for a derived client. ## When to use it Create one client for a service boundary, then use that client for requests to the service. `ky.create()` gives the instance a new set of defaults. `ky.extend()` creates a child instance that inherits the parent's defaults and changes selected values. ## Create a client Install Ky first: ```bash npm install ky ``` The exported [`KyInstance`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky) type describes the reusable client. Create the client at a module boundary that your request code can import: ```ts title="api.ts" import ky, {type KyInstance} from 'ky'; export const api: KyInstance = ky.create({ baseUrl: 'https://api.example.com/', prefix: 'v1/', headers: { accept: 'application/json', 'x-client': 'dashboard', }, retry: 2, timeout: 10_000, }); ``` The client resolves a relative input such as `users` against the base URL after applying the prefix, so this example targets the service's `v1/users` path. The default headers and retry and timeout settings apply to requests made through `api`. ## Override defaults for one request Pass request options to the instance method. A request-level `baseUrl`, `prefix`, header, or retry value takes precedence for that request; wrap `headers` with `replaceOption` when that request must replace the shared headers: ```ts title="users.ts" import {replaceOption} from 'ky'; import {api} from './api.js'; interface User { id: number; name: string; } async function loadUsers(): Promise { const users = await api.get('users', { headers: replaceOption({ accept: 'application/json', 'x-request-id': 'dashboard-users', }), retry: 0, }).json(); const healthApi = api.extend({ headers: replaceOption({ 'x-client': 'health-check', }), }); const health = await healthApi.get('health', { baseUrl: 'https://status.example.com/', prefix: '', }).text(); console.log(users, health); } void loadUsers(); ``` `api.get()` returns a [`ResponsePromise`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky-responsepromise), so `.json()` reads and types the response body without first awaiting a separate response. The second call uses its request-level URL settings instead of the shared service path and returns the body as text. For the URL input and resolution rules for `URL`, `Request`, `baseUrl`, `prefix`, and `searchParams`, see [Instances and defaults](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/instances-and-defaults). This page adds the request-level override pattern: keep shared defaults for ordinary calls, then supply different URL settings for one request. ## Derive a specialized client Use `extend()` when a group of requests needs a variation of the shared client. Normal extension deep-merges options: headers merge, search parameters accumulate, and hooks append. Use [`replaceOption`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky) when the child must replace an inherited merged value rather than combine with it: ```ts title="users-api.ts" import {replaceOption, type KyInstance} from 'ky'; import {api} from './api.js'; export const usersApi: KyInstance = api.extend({ prefix: 'users/', headers: replaceOption({ accept: 'application/json', 'x-client': 'users-service', }), }); async function loadUser(): Promise { const response = await usersApi.get('42').text(); console.log(response); } void loadUser(); ``` The child keeps the parent's `baseUrl` and other defaults, uses `users/` as its prefix, and replaces the inherited header collection with the two headers shown. The original `api` remains unchanged. These settings are fields of [`Options`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky-options). The options that matter here are: | Option | Type | Default | What it does | | --- | --- | --- | --- | | `baseUrl` | `string \| URL` | — | Resolves relative inputs against a base URL. An absolute input bypasses it. | | `prefix` | `string \| URL` | — | Prepends a string or URL to a string input before URL resolution. | | `headers` | `KyHeadersInit` | — | Supplies HTTP headers; child instances merge them unless you use `replaceOption`. | | `retry` | [`RetryOptions`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky-retryoptions) `\| number` | — | Controls retry behavior; a number supplies the retry limit. | | `timeout` | `number \| false` | — | Sets the per-attempt timeout in milliseconds. | | `json` | `unknown` | — | Sends a JSON body instead of constructing `body` yourself. | ## Pitfalls - Include a trailing slash in a path-bearing `baseUrl`, such as `/api/`, when page-relative inputs should extend that path rather than replace its last segment. - `prefix` only affects string inputs. A `URL` or `Request` input does not receive it; a `Request` also ignores `baseUrl`. - `extend()` does not replace merged values by default. Wrap a header, hook, search-parameter, context, or other merged option with `replaceOption` when the child must replace it. ## Related - [`ky`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky) for the complete instance API and URL options. - [`ky-options`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky-options) for the full `Options` type. # Request hooks and authentication Use [`ky`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky) hooks when several requests share authentication, request changes, caching, or error handling. The hooks run at defined points in the request lifecycle, so put each change at the point where its inputs exist. ## Attach authentication and request metadata Create an instance with `beforeRequest` when every request needs the current token. The hook receives the normalized request and can change its headers immediately before Ky sends it. Pass per-request values through `context` rather than adding them to the URL or body. ```ts import ky from 'ky'; const getToken = (): string => 'secret123'; const api = ky.create({ hooks: { beforeRequest: [({request, options}) => { const token = options.context.token; if (typeof token === 'string') { request.headers.set('Authorization', `Bearer ${token}`); } request.headers.set('X-Client', 'example-app'); }], }, }); const run = async (): Promise => { const profile = await api.get('https://api.example.com/profile', { context: {token: getToken()}, }).json<{name: string}>(); console.log(profile.name); }; void run(); ``` The outgoing request has `Authorization: Bearer secret123` and `X-Client: example-app`. `beforeRequest` runs once for the initial request; it does not run again for retries. To alter options before Ky constructs the request, use `init` instead. `init` is synchronous. ## Refresh authentication before a retry Configure the status that can be retried, then refresh the token in `beforeRetry`. This hook runs only after Ky has selected a retry, so a 401 response does not refresh unless 401 is included in `retry.statusCodes`. ```ts import ky from 'ky'; const refreshToken = async (): Promise => 'fresh-token'; let accountAttempts = 0; const api = ky.create({ retry: {statusCodes: [401]}, hooks: { beforeRequest: [() => { if (accountAttempts === 0) { accountAttempts++; return new Response('Unauthorized', {status: 401}); } }], beforeRetry: [async ({request}) => { const token = await refreshToken(); request.headers.set('Authorization', `Bearer ${token}`); }], }, }); const run = async (): Promise => { try { const account = await api.get('https://api.example.com/account').json<{id: string}>(); console.log(account.id); } catch (error) { console.log(error instanceof Error ? error.message : 'Request failed after retry'); } }; void run(); ``` The retry sends the refreshed `Authorization` header and then returns the account response. `retryCount` is at least `1` in this hook. A `beforeRetry` hook can return a replacement `Request`, return a `Response` to skip the retry, throw to propagate an error, or return `ky.stop` to stop without propagating an error. A replacement request is used as-is, so remove credentials before sending it to another origin. ## Return a cached response Return a `Response` from `beforeRequest` to satisfy a request without making an HTTP request. Cache a clone in `afterResponse`, because the response body can only be consumed once. ```ts import ky from 'ky'; const responses = new Map(); responses.set('https://api.example.com/catalog', new Response(JSON.stringify({items: ['cached-item']}), { headers: {'Content-Type': 'application/json'}, })); const api = ky.create({ hooks: { beforeRequest: [({request}) => { const cached = responses.get(request.url); return cached?.clone(); }], afterResponse: [({request, response}) => { responses.set(request.url, response.clone()); return response; }], }, }); const run = async (): Promise => { const catalog = await api.get('https://api.example.com/catalog').json<{items: string[]}>(); console.log(catalog); }; void run(); ``` The call receives a cached clone from the map and does not make an HTTP request. Returning a cached response also skips the remaining `beforeRequest` hooks. ## Transform errors before they are thrown Use `beforeError` to change the error that Ky is about to throw. This adds a final transformation step: return the `Error` you want the caller to receive, as this example changes its name and message. For HTTP-error narrowing and the consumed response-body details, see [Send JSON and read responses](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/json-requests-and-responses#handle-http-failures-separately). ```ts import ky, {isHTTPError} from 'ky'; const api = ky.create({ hooks: { beforeRequest: [() => new Response(JSON.stringify({message: 'Not found'}), { status: 404, headers: {'Content-Type': 'application/json'}, })], beforeError: [({error}) => { if (isHTTPError(error)) { error.name = 'ApiError'; error.message = `Request failed with status ${error.response.status}`; } return error; }], }, }); const run = async (): Promise => { try { await api.get('https://api.example.com/missing').json<{id: string}>(); } catch (error) { console.log(error instanceof Error ? error.message : 'Unknown failure'); } }; void run(); ``` The thrown error has the changed name and message for an HTTP failure. `beforeError` receives Ky lifecycle errors such as HTTP, network, and timeout errors and must return an `Error` instance. Errors raised while a returned response is being consumed are handled through a body shortcut such as `.json()`. ## Choose the hook and preserve defaults Use these hook responsibilities: | Hook | Use it for | Result | | --- | --- | --- | | `init` | Change mutable options before request construction | Options change; the hook is synchronous | | `beforeRequest` | Add authentication or replace the initial request | A request, a response that avoids the network, or no replacement | | `beforeRetry` | Refresh credentials or modify a selected retry | A replacement request, a response that skips retry, `ky.stop`, or no replacement | | `afterResponse` | Inspect or replace a response, or force a retry with `ky.retry()` | Ky uses a returned `Response`; `ky.retry()` starts a retry | | `beforeError` | Rename or replace an error before it is thrown | Ky throws the returned `Error` | When you extend an instance, Ky deep-merges options. That appends hook arrays and merges headers. Use [`replaceOption`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky) when the child instance must replace a merged value instead of inheriting it. ## Related - [Ky options](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky-options) - [Error handling and validation](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/error-handling-and-validation) - [Ky hooks](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky-hooks) # Configure retries, retry codes, and token refresh Use [`ky`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky#default) when you need to decide which failures are retried, identify a forced retry with a machine-readable code, or replace an expired credential before the next attempt. ## Choose when Ky retries Set `retry.limit` and `retry.statusCodes` for the ordinary retry policy. Use [`HTTPError`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky-httperror#httperror) when the policy depends on an HTTP response, and use `shouldRetry` when it also depends on the retry count. It runs only after the method and retry-limit checks, and its result controls the decision: `true` forces a retry, `false` prevents one, and `undefined` keeps Ky's default checks. `beforeRetry` has a different job. Ky calls it after a retry has been selected, so use it to change the request rather than to decide whether the retry happens. ```ts import ky, {HTTPError} from 'ky'; async function main(): Promise { const response = await ky.get('https://api.example.com/report', { retry: { limit: 3, methods: ['get'], statusCodes: [429, 500, 503], shouldRetry: ({error, retryCount}) => { if (error instanceof HTTPError && error.response.status === 429) { return retryCount <= 2; } if (error instanceof HTTPError && error.response.status >= 400 && error.response.status < 500) { return false; } return undefined; }, }, }); console.log(response.status); } void main(); ``` The request retries up to three times for the configured `GET` statuses. A `429` retries only on the first two retry decisions; other client errors do not retry. Returning `undefined` leaves network errors and the configured status-code behavior to Ky's default logic. The default retry delay grows exponentially. Set `retry.delay` to calculate it from the attempt count, `retry.backoffLimit` to cap each delay, and `retry.jitter` to add randomness. A server retry-timing header takes precedence over jitter. Set `retry.retryOnTimeout: true` when a timeout before receiving a response is also retryable. A timeout while a shortcut method reads an already received body is not retried. Retries buffer a streaming request body through `tee()`. That can consume substantial memory, so set `retry: {limit: 0}` for a large streaming upload that does not need retries. ## Force a retry and attach a code The addition here is a response-content-triggered retry—even after a successful HTTP status—with a reason carried into the retry hook. For ordinary retry selection and hook roles, see [How Ky works](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/how-ky-works). ```ts import ky, {isForceRetryError} from 'ky'; const api = ky.extend({ retry: {limit: 2}, hooks: { afterResponse: [ async ({response, retryCount}) => { if (response.status !== 200) { return response; } const data = await response.clone().json<{ready: boolean}>(); if (!data.ready && retryCount === 0) { return ky.retry({code: 'REPORT_NOT_READY'}); } return response; }, ], beforeRetry: [ ({error, retryCount}) => { if (isForceRetryError(error)) { console.log(`Forced retry #${retryCount}: ${error.code}`); } }, ], }, }); async function main(): Promise { const report = await api.get('https://api.example.com/report').json<{ready: boolean}>(); console.log(report.ready); } void main(); ``` When the response says `ready: false` on the first attempt, Ky selects another attempt and the `beforeRetry` hook sees the `REPORT_NOT_READY` code. The sample logs the later response's `ready` field; it is `undefined` if the service omits that field. If the retry limit is exhausted, the forced retry is surfaced as a `ForceRetryError` through Ky's error lifecycle. ## Distinguish the failure you handle The addition here is one caller branch that reports the relevant request detail for each failure. For the guards' meanings, see [How Ky works](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/how-ky-works). ```ts import ky, {isHTTPError, isNetworkError, isTimeoutError} from 'ky'; async function main(): Promise { try { await ky.get('https://api.example.com/account', {timeout: 5_000}).json(); } catch (error) { if (isHTTPError(error)) { console.error('HTTP status:', 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 { throw error; } } } void main(); ``` See [error handling and validation](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/error-handling-and-validation) for the response-error body handling and error model. # 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 => { 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 => { 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 => { 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 => { 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('.') ?? ''; 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) # Cancel requests and report progress Use [`ky`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky#default) with an `AbortSignal` to cancel a request, and use the progress callbacks in [`Options`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky-options#options) to update upload or download status when the runtime supports streaming. The callback receives a [`Progress`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky#progress) object and the current byte chunk. ## When to use this Use cancellation when a view closes, a newer request supersedes an older one, or the user explicitly stops a transfer. Use progress callbacks when a streamed body is large enough that the user needs feedback during the transfer. ## Cancel a request Create an `AbortController`, pass its `signal`, and call `abort()` from the event that stops the work. The request rejects with an abort error, so handle that case separately from other failures. ```ts import ky from 'ky'; const run = async (): Promise => { const controller = new AbortController(); const {signal} = controller; const requestUrl = 'https://example.com/long-running-report'; const request = ky.get(requestUrl, {signal}).text(); setTimeout(() => { controller.abort(); }, 5000); try { const report = await request; console.log(report); } catch (error) { if (error instanceof Error && error.name === 'AbortError') { console.log('Request cancelled'); } else { throw error; } } }; void run(); ``` The request stops when the controller is aborted; the `catch` block identifies that cancellation by the error name and rethrows unrelated errors. ## Report download progress Pass `onDownloadProgress` to the request. Ky calls it as response chunks arrive. `percent` ranges from `0` to `1`, `transferredBytes` counts received bytes, and `totalBytes` is an estimate that can be `0` when the size is unavailable. ```ts import ky from 'ky'; const run = async (): Promise => { const downloadUrl = 'https://example.com/files/archive.zip'; const response = await ky.get(downloadUrl, { onDownloadProgress: (progress, chunk) => { const percentage = Math.round(progress.percent * 100); console.log(`${percentage}%`, progress.transferredBytes, progress.totalBytes, chunk.byteLength); }, }); const archive = await response.blob(); console.log(archive.size); }; void run(); ``` The callback reports each received chunk, including an empty final chunk when the response body is empty. The final progress report has `percent` equal to `1`; `totalBytes` can still reflect an estimate rather than a server-provided exact size. ## Report upload progress Pass the upload body and `onUploadProgress` to a method shortcut such as `ky.post()`. A `Blob` gives Ky a body size it can use for the progress estimate. ```ts import ky from 'ky'; const run = async (): Promise => { const upload = new Blob(['row,quantity\nwidget,12\n'], {type: 'text/csv'}); await ky.post('https://example.com/uploads/inventory.csv', { body: upload, onUploadProgress: (progress, chunk) => { const percentage = Math.round(progress.percent * 100); console.log(`${percentage}%`, progress.transferredBytes, progress.totalBytes, chunk.byteLength); }, }); }; void run(); ``` The callback reports bytes as Ky sends the request body and finishes with a progress value of `1`. The upload callback is silently ignored when request stream support is unavailable, when `keepalive` is `true`, or when the effective request mode is `'no-cors'`; do not use it as the only indication that an upload started or finished in those environments. ## Options | Option | Type | Default | What it does | | --- | --- | --- | --- | | `signal` | `AbortSignal \| null \| undefined` | — | Supplies the signal that aborts the request. | | `onDownloadProgress` | `(progress, chunk) => void` | — | Receives download progress and each received `Uint8Array` chunk. | | `onUploadProgress` | `(progress, chunk) => void` | — | Receives upload progress and each sent `Uint8Array` chunk. | | `keepalive` | `boolean` | — | When `true`, prevents Ky from using a streamed request body for upload progress. | ## Pitfalls - Cancellation is cooperative: keep the controller and call `abort()` from the owner of the request, such as a cancel button or a component cleanup handler. - `totalBytes` can be `0` when the transfer size cannot be determined. Display transferred bytes or an indeterminate indicator instead of treating `0` as a zero-byte transfer. - Upload progress depends on request-stream support and HTTP/2 for HTTPS connections in Chromium-based browsers. Check the runtime requirements before making a determinate upload progress bar part of the interface. For reusable URL or retry defaults, see [shared clients and URL defaults](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/shared-clients-and-url-defaults). For Ky's failure categories, see [Ky's error model](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky-error-model). # ky Tiny and elegant HTTP client based on the Fetch API ## Install ```bash npm install ky ``` ## On their own pages - [`Hooks`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky-hooks) - [`HTTPError`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky-httperror): Error thrown when the response has a non-2xx status code and `throwHttpErrors` is enabled. - [`KyRequest`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky-kyrequest) - [`NormalizedOptions`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky-normalizedoptions): Normalized options passed to the `fetch` call and hooks. - [`Options`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky-options): Options are the same as `window.fetch`, except for the KyOptions - [`ResponsePromise`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky-responsepromise) - [`RetryOptions`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky-retryoptions) ## Functions ### `isForceRetryError` Type guard to check if an error is a `ForceRetryError`. ```ts function isForceRetryError(error: unknown): error is ForceRetryError ``` **Parameters** - `error`: The error to check **Returns** `true` if the error is a `ForceRetryError`, `false` otherwise **Example** ``` import ky, {isForceRetryError} from 'ky'; const api = ky.extend({ hooks: { beforeRetry: [ ({error, retryCount}) => { if (isForceRetryError(error)) { console.log(`Forced retry #${retryCount}: ${error.code}`); } } ] } }); ``` ### `isHTTPError` Type guard to check if an error is an `HTTPError`. ```ts function isHTTPError(error: unknown): error is HTTPError ``` **Parameters** - `error`: The error to check **Returns** `true` if the error is an `HTTPError`, `false` otherwise **Example** ``` import ky, {isHTTPError} from 'ky'; try { const response = await ky.get('/api/data'); } catch (error) { if (isHTTPError(error)) { console.log('HTTP error status:', error.response.status); } } ``` ### `isKyError` Type guard to check if an error is a `KyError`. Note: `SchemaValidationError` is intentionally not considered a Ky error. `KyError` covers failures in Ky's HTTP lifecycle (bad status, timeout, retry), while schema validation errors originate from the user-provided schema, not from Ky itself. ```ts function isKyError(error: unknown): error is KyError ``` **Parameters** - `error`: The error to check **Returns** `true` if the error is a Ky error, `false` otherwise **Example** ``` import ky, {isKyError} from 'ky'; try { const response = await ky.get('/api/data'); } catch (error) { if (isKyError(error)) { // Handle Ky-specific errors console.log('Ky error occurred:', error.message); } else { // Handle other errors console.log('Unknown error:', error); } } ``` ### `isNetworkError` Type guard to check if an error is a `NetworkError`. ```ts function isNetworkError(error: unknown): error is NetworkError ``` **Parameters** - `error`: The error to check **Returns** `true` if the error is a `NetworkError`, `false` otherwise **Example** ``` import ky, {isNetworkError} from 'ky'; try { const response = await ky.get('/api/data'); } catch (error) { if (isNetworkError(error)) { console.log('Network error:', error.request.url); } } ``` ### `isResponseSizeError` **Not released yet.** It is in the source, not in the latest release on npm. Type guard to check if an error is a `ResponseSizeError`. ```ts function isResponseSizeError(error: unknown): error is ResponseSizeError ``` **Parameters** - `error`: The error to check **Returns** `true` if the error is a `ResponseSizeError`, `false` otherwise **Example** ``` import ky, {isResponseSizeError} from 'ky'; try { await ky('https://example.com/data', {maxResponseSize: 1024}).json(); } catch (error) { if (isResponseSizeError(error)) { console.log(`Response exceeded ${error.maxResponseSize} bytes`); } } ``` ### `isTimeoutError` Type guard to check if an error is a `TimeoutError`. ```ts function isTimeoutError(error: unknown): error is TimeoutError ``` **Parameters** - `error`: The error to check **Returns** `true` if the error is a `TimeoutError`, `false` otherwise **Example** ``` import ky, {isTimeoutError} from 'ky'; try { const response = await ky.get('/api/data', { timeout: 1000 }); } catch (error) { if (isTimeoutError(error)) { console.log('Request timed out:', error.request.url); } } ``` ## Classes ### `ForceRetryError` Error used to signal a forced retry from `afterResponse` hooks. This is thrown when `ky.retry()` is returned from an `afterResponse` hook. It is observable in `beforeRetry` and `beforeError` hooks via the `isForceRetryError()` type guard. ```ts class ForceRetryError extends KyError ``` **Properties** | Name | Type | Default | Description | | --- | --- | --- | --- | | `name` | | `'ForceRetryError'` | | | `customDelay` | `number \| undefined` | | | | `code` | `string \| undefined` | | | | `customRequest` | `Request \| undefined` | | | | `message` | `string` | | | | `stack?` | `string` | | | **Methods** - `constructor(options?: ForceRetryOptions)` ### `KyError` Base class for all Ky-specific errors. `HTTPError`, `NetworkError`, `TimeoutError`, `ResponseSizeError`, and `ForceRetryError` extend this class. You can use `instanceof KyError` to check if an error originated from Ky, or use the `isKyError()` type guard for cross-realm compatibility and TypeScript type narrowing. Note: `SchemaValidationError` is intentionally not considered a Ky error. `KyError` covers failures in Ky's HTTP lifecycle (bad status, timeout, retry), while schema validation errors originate from the user-provided schema, not from Ky itself. ```ts class KyError extends Error ``` **Properties** | Name | Type | Default | Description | | --- | --- | --- | --- | | `name` | | `'KyError'` | | | `message` | `string` | | | | `stack?` | `string` | | | **Methods** - `get isKyError(): true` ### `NetworkError` Error thrown when a network error occurs during the request (e.g., DNS failure, connection refused, offline). It has a `request` property with the `Request` object. The original error is available via the standard `cause` property. Network errors are automatically retried (for retriable methods). A connection that drops while a Ky shortcut method like `.json()` is reading the response body is also wrapped in `NetworkError`, but it is not retried because the response has already been received. Note: Network errors are detected using runtime-specific heuristics. Unrecognized runtimes may produce errors that are not wrapped in `NetworkError`. Use the `shouldRetry` option to handle such cases. ```ts class NetworkError extends KyError ``` **Properties** | Name | Type | Default | Description | | --- | --- | --- | --- | | `name` | | `'NetworkError'` | | | `request` | `KyRequest` | | | | `message` | `string` | | | | `stack?` | `string` | | | **Methods** - `constructor(request: Request, options?: {cause?: Error | undefined})` ### `ResponseSizeError` **Not released yet.** It is in the source, not in the latest release on npm. Error thrown when the response body exceeds `maxResponseSize`. It has a `request` property with the `Request` object and a `maxResponseSize` property with the configured limit in bytes. ```ts class ResponseSizeError extends KyError ``` **Properties** | Name | Type | Default | Description | | --- | --- | --- | --- | | `name` | | `'ResponseSizeError'` | | | `request` | `KyRequest` | | | | `maxResponseSize` | `number` | | | | `message` | `string` | | | | `stack?` | `string` | | | **Methods** - `constructor(request: Request, maxResponseSize: number)` **Example** ``` import ky, {isResponseSizeError} from 'ky'; try { await ky('https://example.com/data', {maxResponseSize: 1024}).json(); } catch (error) { if (isResponseSizeError(error)) { console.log(`Response exceeded ${error.maxResponseSize} bytes`); } } ``` ### `SchemaValidationError` The error thrown when [Standard Schema](https://github.com/standard-schema/standard-schema) validation fails in `.json(schema)`. It has an `issues` property with the validation issues from the schema. This error intentionally does not extend `KyError` because it does not represent a failure in Ky's HTTP lifecycle. The request succeeded; the user's schema rejected the data. As such, it is not matched by `isKyError()`. ```ts class SchemaValidationError extends Error ``` **Properties** | Name | Type | Default | Description | | --- | --- | --- | --- | | `name` | | `'SchemaValidationError'` | | | `issues` | `readonly StandardSchemaV1Issue[]` | | | | `message` | `string` | | | | `stack?` | `string` | | | **Methods** - `constructor(issues: readonly StandardSchemaV1Issue[])` **Example** ``` import ky, {SchemaValidationError} from 'ky'; import {z} from 'zod'; const userSchema = z.object({name: z.string()}); try { const user = await ky('/api/user').json(userSchema); console.log(user.name); } catch (error) { if (error instanceof SchemaValidationError) { console.error(error.issues); } } ``` ### `TimeoutError` Error thrown when the request times out. It has a `request` property with the `Request` object. ```ts class TimeoutError extends KyError ``` **Properties** | Name | Type | Default | Description | | --- | --- | --- | --- | | `name` | | `'TimeoutError'` | | | `request` | `KyRequest` | | | | `message` | `string` | | | | `stack?` | `string` | | | **Methods** - `constructor(request: Request)` ## Constants ### `default` The package's default export: import it under a name of your own, `import ky from 'ky'`. ```ts declare const ky: KyInstance export default ky ``` ### `replaceOption` Wraps a value so that `ky.extend()` will replace the parent value instead of merging with it. Works with hooks, headers, search parameters, context, and any other deep-merged option. By default, `.extend()` deep-merges options with the parent instance: hooks get appended, headers get merged, and search parameters get accumulated. Use `replaceOption` when you want to fully replace a merged property instead. ```ts const replaceOption: (value: T) => T ``` **Example** ``` import ky, {replaceOption} from 'ky'; const base = ky.create({ hooks: {beforeRequest: [addAuth, addTracking]}, }); // Replaces instead of appending const extended = base.extend({ hooks: replaceOption({beforeRequest: [onlyThis]}), }); // hooks.beforeRequest is now [onlyThis], not [addAuth, addTracking, onlyThis] ``` ## Types ### `AfterResponseHook` ```ts type AfterResponseHook = (state: AfterResponseState) => Response | RetryMarker | void | Promise; ``` ### `AfterResponseState` ```ts type AfterResponseState = { … } ``` **Properties** | Name | Type | Default | Description | | --- | --- | --- | --- | | `request` | `KyRequest` | | | | `options` | `Readonly` | | | | `response` | `KyResponse` | | | | `retryCount` | `number` | | The number of retries attempted. `0` for the initial request, increments with each retry. | ### `BeforeErrorHook` ```ts type BeforeErrorHook = (state: BeforeErrorState) => Error | Promise; ``` ### `BeforeErrorState` ```ts type BeforeErrorState = { … } ``` **Properties** | Name | Type | Default | Description | | --- | --- | --- | --- | | `request` | `KyRequest` | | | | `options` | `Readonly` | | | | `error` | `Error` | | | | `retryCount` | `number` | | The number of retries attempted. `0` for the initial request, increments with each retry. | ### `BeforeRequestHook` ```ts type BeforeRequestHook = (state: BeforeRequestState) => Request | Response | void | Promise; ``` ### `BeforeRequestState` ```ts type BeforeRequestState = { … } ``` **Properties** | Name | Type | Default | Description | | --- | --- | --- | --- | | `request` | `KyRequest` | | | | `options` | `Readonly` | | | | `retryCount` | `0` | | The number of retries attempted. Always `0`, since `beforeRequest` hooks run once before retry handling begins. | ### `BeforeRetryHook` ```ts type BeforeRetryHook = (state: BeforeRetryState) => Request | Response | typeof stop | void | Promise; ``` ### `BeforeRetryState` ```ts type BeforeRetryState = { … } ``` **Properties** | Name | Type | Default | Description | | --- | --- | --- | --- | | `request` | `KyRequest` | | | | `options` | `Readonly` | | | | `error` | `Error` | | | | `retryCount` | `number` | | The number of retries attempted. Always `>= 1`, since this hook is only called during retries, not on the initial request. | ### `InitHook` This hook enables you to modify the options before they are used to construct the request. The hook function receives the mutable options object and can modify it in place. You could, for example, modify `searchParams`, `headers`, or `json` here. The `headers` option is always a plain object with lowercase names, where a header removed with `undefined` keeps an `undefined` value. Unlike other hooks, `init` hooks are synchronous. Any error thrown will propagate synchronously and will not be caught by `beforeError` hooks. ```ts type InitHook = (options: InitOptions) => void; ``` **Example** ``` import ky from 'ky'; const api = ky.extend({ hooks: { init: [ options => { options.searchParams = {apiKey: getApiKey()}; }, ], }, }); const response = await api.get('https://example.com/api/users'); // URL: https://example.com/api/users?apiKey=123 ``` ### `Input` ```ts type Input = string | URL | Request; ``` **Members** - `toString(): string` — Returns a string representation of a string. - `valueOf(): string` — Returns the primitive value of the specified object. ### `KyInstance` ```ts type KyInstance = { … } ``` **Properties** | Name | Type | Default | Description | | --- | --- | --- | --- | | `get` | `(url: Input, options?: Options) => ResponsePromise` | | Fetch the given `url` using the option `{method: 'get'}`. | | `post` | `(url: Input, options?: Options) => ResponsePromise` | | Fetch the given `url` using the option `{method: 'post'}`. | | `put` | `(url: Input, options?: Options) => ResponsePromise` | | Fetch the given `url` using the option `{method: 'put'}`. | | `delete` | `(url: Input, options?: Options) => ResponsePromise` | | Fetch the given `url` using the option `{method: 'delete'}`. | | `patch` | `(url: Input, options?: Options) => ResponsePromise` | | Fetch the given `url` using the option `{method: 'patch'}`. | | `head` | `(url: Input, options?: Options) => ResponsePromise` | | Fetch the given `url` using the option `{method: 'head'}`. | | `query` | `(url: Input, options?: Options) => ResponsePromise` | | Fetch the given `url` using the option `{method: 'query'}`. | | `create` | `(defaultOptions?: Options) => KyInstance` | | Create a new Ky instance with complete new defaults, without inheriting from any parent instance. | | `extend` | `(defaultOptions: Options \| ((parentOptions: Options) => Options)) => KyInstance` | | Create a new Ky instance with some defaults overridden with your own. | | `stop` | `typeof stop` | | A `Symbol` that can be returned by a `beforeRetry` hook to stop the retry. This will also short circuit the remaining `beforeRetry` hooks. | | `retry` | `typeof retry` | | Force a retry from an `afterResponse` hook. | ### `KyResponse` ```ts type KyResponse = { clone: () => KyResponse; json: () => Promise; } & Response; ``` **Properties** | Name | Type | Default | Description | | --- | --- | --- | --- | | `clone` | `() => KyResponse` | | | | `json` | `() => Promise` | | | | `headers` | `Headers` | | The **`headers`** read-only property of the with the response. | | `ok` | `boolean` | | The **`ok`** read-only property of the Response interface contains a Boolean stating whether the response was successful (status in the range 200-299) or not. | | `redirected` | `boolean` | | The **`redirected`** read-only property of the Response interface indicates whether or not the response is the result of a request you made which was redirected. | | `status` | `number` | | The **`status`** read-only property of the Response interface contains the HTTP status codes of the response. | | `statusText` | `string` | | The **`statusText`** read-only property of the Response interface contains the status message corresponding to the HTTP status code in Response.status. | | `type` | `ResponseType` | | The **`type`** read-only property of the Response interface contains the type of the response. | | `url` | `string` | | The **`url`** read-only property of the Response interface contains the URL of the response. | | `body` | `ReadableStream> \| null` | | [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/body) | | `bodyUsed` | `boolean` | | [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/bodyUsed) | **Members** - `arrayBuffer(): Promise` — [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/arrayBuffer) - `blob(): Promise` — [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/blob) - `bytes(): Promise>` — [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/bytes) - `formData(): Promise` — [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/formData) - `text(): Promise` — [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/text) ### `Progress` ```ts type Progress = { … } ``` **Properties** | Name | Type | Default | Description | | --- | --- | --- | --- | | `percent` | `number` | | A number between `0` and `1` representing the progress percentage. | | `transferredBytes` | `number` | | The number of bytes transferred so far. | | `totalBytes` | `number` | | The total number of bytes to be transferred. This is an estimate and may be `0` for an empty transfer or when the total size cannot be determined. | ### `SearchParamsOption` ```ts type SearchParamsOption = | Exclude | Record | Array> | ReadonlyArray>; ``` **Members** - `toString(): string` — Returns a string representation of a string. - `valueOf(): string` — Returns the primitive value of the specified object. ### `ShouldRetryState` ```ts type ShouldRetryState = { … } ``` **Properties** | Name | Type | Default | Description | | --- | --- | --- | --- | | `error` | `Error` | | The error that caused the request to fail. | | `retryCount` | `number` | | The number of retries attempted. Starts at 1 for the first retry. | ### `StandardSchemaV1` ```ts type StandardSchemaV1 = { … } ``` **Properties** | Name | Type | Default | Description | | --- | --- | --- | --- | | `'~standard'` | `{ readonly version: 1; readonly vendor: string; readonly validate: ( value: unknown, options?: StandardSchemaV1Options, ) => StandardSchemaV1Result \| Promise>; readonly types?: StandardSchemaV1Types \| undefined; }` | | | | `'~standard'` | `{ readonly version: 1; readonly vendor: string; readonly validate: ( value: unknown, options?: StandardSchemaV1Options, ) => StandardSchemaV1Result \| Promise>; readonly types?: StandardSchemaV1Types \| undefined; }` | | | ### `StandardSchemaV1InferOutput` ```ts type StandardSchemaV1InferOutput = Schema['~standard'] extends { readonly types: StandardSchemaV1Types; } ? OutputType : Extract< Awaited>, StandardSchemaV1SuccessResult > extends StandardSchemaV1SuccessResult ? OutputType : unknown; ``` ### `StandardSchemaV1Issue` ```ts type StandardSchemaV1Issue = { … } ``` **Properties** | Name | Type | Default | Description | | --- | --- | --- | --- | | `message` | `string` | | | | `path?` | `ReadonlyArray \| undefined` | | | # Hooks Import it from `ky`. ```ts type Hooks = { … } ``` **Properties** | Name | Type | Default | Description | | --- | --- | --- | --- | | `init?` | `readonly InitHook[] \| undefined` | `[]` | This hook enables you to modify the options before they are used to construct the request. The hook function receives the mutable options object and can modify it in place. You could, for example, modify `searchParams`, `headers`, or `json` here. The `headers` option is always a plain object with lowercase names, where a header removed with `undefined` keeps an `undefined` value. | | `beforeRequest?` | `readonly BeforeRequestHook[] \| undefined` | `[]` | This hook enables you to modify the request right before it is sent. Ky will make no further changes to the request after this. The hook function receives a state object with the normalized request, options, and retry count. You could, for example, modify `request.headers` here. | | `beforeRetry?` | `readonly BeforeRetryHook[] \| undefined` | `[]` | This hook enables you to modify the request right before retry. Ky will make no further changes to the request after this. The hook function receives a state object with the normalized request, options, an error instance, and retry count. You could, for example, modify `request.headers` here. | | `beforeError?` | `readonly BeforeErrorHook[] \| undefined` | `[]` | This hook enables you to modify any error right before it is thrown. The hook function receives a state object with the current request, the normalized Ky options, the error, and retry count, and should return an `Error` instance. | | `afterResponse?` | `readonly AfterResponseHook[] \| undefined` | `[]` | This hook enables you to read and optionally modify the response. The hook function receives a state object with the normalized request, options, a clone of the response, and retry count. The return value of the hook function will be used by Ky as the response object if it's an instance of [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response). | # HTTPError Import it from `ky`. Error thrown when the response has a non-2xx status code and `throwHttpErrors` is enabled. The error has a `response` property with the `Response` object, a `request` property with the `Request` object, an `options` property with the normalized options (either passed to `ky` when creating an instance with `ky.create()` or directly when performing the request), and a `data` property with the pre-parsed response body. For JSON responses (based on `Content-Type`), the body is parsed using the `parseJson` option if set, or `JSON.parse` by default. For other content types, it is set as plain text. If the body is empty, unreadable, too large, parsing fails, or the error-data read/parse timeout is reached, `data` will be `undefined`. To avoid hanging or excessive buffering, `error.data` body reads and async JSON parsing are bounded by the request timeout (or 10 seconds when `timeout` is disabled), any remaining `totalTimeout` budget, and a 10 MiB response body size limit. If `maxResponseSize` is exceeded while populating `error.data`, Ky throws `ResponseSizeError` instead of `HTTPError`. If `totalTimeout` expires while populating `error.data`, Ky throws `TimeoutError` instead of `HTTPError`. The `data` property is populated before `beforeError` hooks run, so hooks can access it. The response body is automatically consumed when populating `error.data`, so `error.response.json()` and other body methods will not work. Use `error.data` instead. The `error.response` object is still available for headers, status, etc. Be aware that some types of errors, such as network errors, inherently mean that a response was not received. In that case, the error will be an instance of `NetworkError` instead of `HTTPError` and will not contain a `response` property. ```ts class HTTPError extends KyError ``` **Properties** | Name | Type | Default | Description | | --- | --- | --- | --- | | `name` | | `'HTTPError'` | | | `response` | `KyResponse` | | | | `request` | `KyRequest` | | | | `options` | `Readonly` | | | | `data` | `T \| string \| undefined` | | | | `message` | `string` | | | | `stack?` | `string` | | | **Methods** - `constructor(response: Response, request: Request, options: Readonly)` # KyRequest Import it from `ky`. ```ts type KyRequest = { clone: () => KyRequest; json: () => Promise; } & Request; ``` **Properties** | Name | Type | Default | Description | | --- | --- | --- | --- | | `clone` | `() => KyRequest` | | | | `json` | `() => Promise` | | | | `cache` | `RequestCache` | | The **`cache`** read-only property of the Request interface contains the cache mode of the request. | | `credentials` | `RequestCredentials` | | The **`credentials`** read-only property of the Request interface reflects the value given to the Request.Request() constructor in the `credentials` option. | | `destination` | `RequestDestination` | | The **`destination`** read-only property of the **Request** interface returns a string describing the type of content being requested. | | `headers` | `Headers` | | The **`headers`** read-only property of the with the request. | | `integrity` | `string` | | The **`integrity`** read-only property of the Request interface contains the subresource integrity value of the request. | | `keepalive` | `boolean` | | The **`keepalive`** read-only property of the Request interface contains the request's `keepalive` setting (`true` or `false`), which indicates whether the browser will keep the associated request alive if the page that initiated it is unloaded before the request is complete. | | `method` | `string` | | The **`method`** read-only property of the `POST`, etc.) A String indicating the method of the request. | | `mode` | `RequestMode` | | The **`mode`** read-only property of the Request interface contains the mode of the request (e.g., `cors`, `no-cors`, `same-origin`, or `navigate`.) This is used to determine if cross-origin requests lead to valid responses, and which properties of the response are readable. | | `redirect` | `RequestRedirect` | | The **`redirect`** read-only property of the Request interface contains the mode for how redirects are handled. | | `referrer` | `string` | | The **`referrer`** read-only property of the Request. | | `referrerPolicy` | `ReferrerPolicy` | | The **`referrerPolicy`** read-only property of the referrer information, sent in the Referer header, should be included with the request. | | `signal` | `AbortSignal` | | The read-only **`signal`** property of the Request interface returns the AbortSignal associated with the request. | | `url` | `string` | | The **`url`** read-only property of the Request interface contains the URL of the request. | | `body` | `ReadableStream> \| null` | | [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/body) | | `bodyUsed` | `boolean` | | [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/bodyUsed) | **Members** - `arrayBuffer(): Promise` — [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/arrayBuffer) - `blob(): Promise` — [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/blob) - `bytes(): Promise>` — [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/bytes) - `formData(): Promise` — [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/formData) - `text(): Promise` — [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/text) # NormalizedOptions Import it from `ky`. Normalized options passed to the `fetch` call and hooks. ```ts interface NormalizedOptions extends Readonly ``` **Properties** | Name | Type | Default | Description | | --- | --- | --- | --- | | `method` | `NonNullable` | | A string to set request's method. | | `credentials?` | `NonNullable` | | A string indicating whether credentials will be sent with the request always, never, or only when sent to a same-origin URL. Sets request's credentials. | | `headers` | `Headers` | | A Headers object, an object literal, or an array of two-item arrays to set request's headers. | | `retry` | `NormalizedRetryOptions` | | | | `baseUrl?` | `Options['baseUrl']` | | | | `prefix` | `string` | | | | `onDownloadProgress?` | `NonNullable` | | | | `onUploadProgress?` | `NonNullable` | | | | `context` | `Record` | | | | `body?` | `BodyInit \| null` | | A BodyInit object or null to set request's body. | | `cache?` | `RequestCache` | | A string indicating how the request will interact with the browser's cache to set request's cache. | | `integrity?` | `string` | | A cryptographic hash of the resource to be fetched by request. Sets request's integrity. | | `keepalive?` | `boolean` | | A boolean to set request's keepalive. | | `mode?` | `RequestMode` | | A string to indicate whether the request will use CORS, or will be restricted to same-origin URLs. Sets request's mode. | | `priority?` | `RequestPriority` | | | | `redirect?` | `RequestRedirect` | | A string indicating whether request follows redirects, results in an error upon encountering a redirect, or returns the redirect (in an opaque fashion). Sets request's redirect. | | `referrer?` | `string` | | A string whose value is a same-origin URL, "about:client", or the empty string, to set request's referrer. | | `referrerPolicy?` | `ReferrerPolicy` | | A referrer policy to set request's referrerPolicy. | | `signal?` | `AbortSignal \| null` | | An AbortSignal to set request's signal. | | `window?` | `null` | | Can only be null. Used to disassociate request from any Window. | # Options Import it from `ky`. Options are the same as `window.fetch`, except for the KyOptions ```ts interface Options extends KyOptions, RequestOptions ``` **Properties** | Name | Type | Default | Description | | --- | --- | --- | --- | | `method?` | `LiteralUnion \| undefined` | | HTTP method used to make the request. | | `headers?` | `KyHeadersInit \| undefined` | | HTTP headers used to make the request. | | `signal?` | `AbortSignal \| null \| undefined` | | An `AbortSignal` to abort the request. | | `json?` | `unknown` | | Shortcut for sending JSON. Use this instead of the `body` option. | | `parseJson?` | `((text: string, context: {request: Request; response: Response}) => unknown) \| undefined` | `JSON.parse()` | User-defined JSON-parsing function. | | `stringifyJson?` | `((data: unknown) => string) \| undefined` | `JSON.stringify()` | User-defined JSON-stringifying function. | | `searchParams?` | `SearchParamsOption` | | Search parameters to include in the request URL. Setting this will merge with any existing search parameters in the input URL. | | `baseUrl?` | `URL \| string \| undefined` | | A base URL to [resolve](https://developer.mozilla.org/en-US/docs/Web/API/URL_API/Resolving_relative_references) the `input` against. When the `input` (after applying the `prefix` option) is only a relative URL, such as `'users'`, `'/users'`, or `'//my-site.com'`, it will be resolved against the `baseUrl` to determine the destination of the request. | | `prefix?` | `URL \| string \| undefined` | | A prefix to prepend to the `input` before making the request (and before it is resolved against the `baseUrl`). It can be any valid path or URL, either relative or absolute. A trailing slash `/` is optional and will be added automatically, if needed, when it is joined with `input`. Only takes effect when `input` is a string. | | `retry?` | `RetryOptions \| number \| undefined` | | Controls retry behavior. Each field is documented in the `RetryOptions` type. | | `timeout?` | `number \| false \| undefined` | `10000` | Per-attempt timeout in milliseconds for getting a response, applied independently to each retry. Ky shortcut methods also use this value as a separate timeout for reading the response body. Cannot be greater than 2147483647. See also `totalTimeout`. | | `totalTimeout?` | `number \| false \| undefined` | `false` | Overall timeout in milliseconds for the entire operation, including retries and delays. Throws a `TimeoutError` if exceeded. Cannot be greater than 2147483647. | | `maxResponseSize?` (not released yet) | `number \| undefined` | `Infinity` | Maximum response body size in bytes. Must be a non-negative safe integer or `Infinity`. Set to `0` to allow only empty bodies. | | `hooks?` | `Hooks \| undefined` | | Hooks allow modifications during the request lifecycle. Hook functions may be async and are run serially, unless otherwise noted. | | `throwHttpErrors?` | `boolean \| ((status: number) => boolean) \| undefined` | `true` | Throw an `HTTPError` when, after following redirects, the response has a non-2xx status code. To also throw for redirects instead of following them, set the [`redirect`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters) option to `'manual'`. | | `onDownloadProgress?` | `((progress: Progress, chunk: Uint8Array) => void) \| undefined` | | Download progress event handler. | | `onUploadProgress?` | `((progress: Progress, chunk: Uint8Array) => void) \| undefined` | | Upload progress event handler. | | `fetch?` | `((input: Request, init?: RequestInit) => Promise) \| undefined` | `fetch` | User-defined `fetch` function. Has to be fully compatible with the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) standard. | | `context?` | `Record \| undefined` | `{}` | User-defined data passed to hooks. | # ResponsePromise Import it from `ky`. ```ts type ResponsePromise = { arrayBuffer: () => Promise; blob: () => Promise; formData: () => Promise; bytes: () => Promise>; json: { (schema?: undefined): Promise; (schema: Schema): Promise>; }; text: () => Promise; } & Promise>; ``` **Properties** | Name | Type | Default | Description | | --- | --- | --- | --- | | `arrayBuffer` | `() => Promise` | | | | `blob` | `() => Promise` | | | | `formData` | `() => Promise` | | | | `bytes` | `() => Promise>` | | Get the response body as raw bytes. | | `json` | `{ /** Get the response body as JSON. @example ``` import ky from 'ky'; const json = await ky(…).json(); ``` @example ``` import ky from 'ky'; interface Result { value: number; } const result1 = await ky(…).json(); // or const result2 = await ky(…).json(); ``` */ (schema?: undefined): Promise; /** Get the response body as JSON and validate it with a Standard Schema. Use a Standard Schema compatible validator (for example, Zod 3.24+). Throws a `SchemaValidationError` when validation fails. @example ``` import ky from 'ky'; import {z} from 'zod'; const userSchema = z.object({name: z.string()}); const user = await ky('/api/user').json(userSchema); ``` */ (schema: Schema): Promise>; }` | | | | `text` | `() => Promise` | | | **Members** - `then(onfulfilled?: ((value: T) => TResult1 | PromiseLike) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike) | undefined | null): Promise` — Attaches callbacks for the resolution and/or rejection of the Promise. - `catch(onrejected?: ((reason: any) => TResult | PromiseLike) | undefined | null): Promise` — Attaches a callback for only the rejection of the Promise. # RetryOptions Import it from `ky`. ```ts type RetryOptions = { … } ``` **Properties** | Name | Type | Default | Description | | --- | --- | --- | --- | | `limit?` | `number \| undefined` | `2` | The number of times to retry failed requests. Must be a finite, non-negative integer. | | `methods?` | `readonly HttpMethod[] \| undefined` | `['get', 'put', 'head', 'delete', 'options', 'trace', 'query']` | The HTTP methods allowed to retry. | | `statusCodes?` | `readonly number[] \| undefined` | `[408, 413, 429, 500, 502, 503, 504]` | The HTTP status codes allowed to retry. | | `afterStatusCodes?` | `readonly number[] \| undefined` | `[413, 429, 503]` | The retriable HTTP status codes that should respect retry timing headers. These status codes must also be included in `statusCodes`. | | `maxRetryAfter?` | `number \| undefined` | `Infinity` | If the retry delay from a retry timing header is greater than `maxRetryAfter`, Ky will use `maxRetryAfter`. | | `backoffLimit?` | `number \| undefined` | `Infinity` | The upper limit of the delay per retry in milliseconds. To clamp the delay, set `backoffLimit` to 1000, for example. | | `delay?` | `((attemptCount: number) => number) \| undefined` | `attemptCount => 0.3 * (2 ** (attemptCount - 1)) * 1000` | A function to calculate the delay in milliseconds between retries given `attemptCount` (starts from 1). | | `jitter?` | `boolean \| ((delay: number) => number) \| undefined` | `undefined (no jitter)` | Add random jitter to retry delays to prevent thundering herd problems. | | `retryOnTimeout?` | `boolean \| undefined` | `false` | Whether to retry when the request times out before a response is returned. Timeouts while reading a response body through Ky shortcut methods are not retried because the response has already been received. | | `shouldRetry?` | `((state: ShouldRetryState) => boolean \| void \| Promise) \| undefined` | `undefined` | A function to determine whether a retry should be attempted. |