# ky · GPT-6 Luna # Ky quick start Install Ky, send a JSON request with a custom header, and read the JSON response body. Ky is an HTTP client based on the Fetch API: pass a URL and options, then use a method shortcut and a body method to make a request and read its response. ## Prerequisites Ky targets modern browsers, Node.js, Bun, and Deno. The package requires Node.js 22 or newer. ## Install ```bash npm install ky ``` ## Send a JSON request 1. Import the default [`ky`](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/ky#default) export. Use `json` for the request body and `headers` for the custom request header. The `post()` shortcut sends a POST request; `json()` reads and parses the response body. ```ts import ky from 'ky'; const main = async (): Promise => { const result = await ky.post('https://httpbin.org/anything', { json: {name: 'Ada'}, headers: { 'x-client': 'ky-quick-start', }, }).json(); console.log(result); }; void main(); ``` Inspect `result` in the console to see the parsed JSON response body. Its type is `unknown` by default; use a type argument when you know the response shape, or validate the response as described in [Send and read JSON](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/send-and-read-json). ## What you have Your code sends a POST request with a JSON body and the `x-client` header, then logs the parsed response body. Ky sets the JSON content type for the `json` option unless you provide a `Content-Type` header yourself. A non-2xx response throws an `HTTPError` by default, and `.json()` also throws if the response body is empty. ## Where to go next - [Send and read JSON](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/send-and-read-json) for response typing and JSON handling. - [Add request headers](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/add-request-headers) for reusable headers. - [How Ky works](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/how-ky-works) for the core concepts. # How Ky works Ky is an HTTP client based on Fetch: pass Fetch-compatible input and options, then use method shortcuts and response-body methods to write common requests directly. ```mermaid flowchart LR A["ky instance + request options"] --> B["init hooks"] B --> C["beforeRequest hooks"] C --> D["Fetch attempt"] D --> E["afterResponse hooks"] E --> F{"Response accepted?"} F -->|"yes"| G["Response and body shortcut"] F -->|"no"| H["retry rules / shouldRetry"] H -->|"retry"| I["beforeRetry hooks"] I --> D H -->|"stop"| J["Ky error"] ``` ## Start with Fetch, then use Ky's shortcuts The default [`ky`](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/ky#default) export accepts the same input and options as Fetch. Its method shortcuts set the HTTP method, and its response promise exposes body methods such as `.json()` without requiring you to await a `Response` first. The JSON result defaults to `unknown`, rather than `any`; a non-2xx response throws an [`HTTPError`](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/ky-httperror#httperror) by default after redirects are followed. ```ts import ky from 'ky'; async function main(): Promise { const user = await ky.get('https://api.example.com/users/42').json(); console.log(user); } void main(); ``` This sends a GET request and resolves `user` with the parsed response body. Check the value's shape before treating the default `unknown` result as a particular application type. ## Give each use case its own defaults Use `ky.create()` to start an instance with its own defaults. Use `extend()` when a use case needs to inherit and adjust a parent instance's defaults. Extension merges options by default: headers merge and hook arrays append. Wrap a merged option with [`replaceOption`](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/ky#replaceoption) when the child must replace it instead. ```ts import ky, {replaceOption} from 'ky'; const api = ky.create({ baseUrl: 'https://api.example.com/v1/', headers: {'x-service': 'catalog'}, hooks: { beforeRequest: [({request}) => { request.headers.set('x-client', 'web'); }], }, }); const usersApi = api.extend({ hooks: { beforeRequest: [({request}) => { request.headers.set('x-feature', 'users'); }], }, }); const isolatedApi = api.extend({ hooks: replaceOption({ beforeRequest: [({request}) => { request.headers.set('x-feature', 'isolated-users'); }], }), }); async function main(): Promise { const user = await usersApi.get('users/42').json(); console.log(user); const isolatedUser = await isolatedApi.get('users/43').json(); console.log(isolatedUser); } void main(); ``` Both requests resolve to their parsed response bodies. The first request uses the inherited service header and both `beforeRequest` hooks; the second uses the service header and only the replacement hook. `api` keeps its own defaults in either case. ## Put request work at the right hook stage Hooks run at defined points in the request lifecycle. `beforeRequest` runs before the initial request is sent, while `afterResponse` receives a clone that you can inspect and may replace by returning a `Response`. Hook functions run serially. Use the stage that matches the action rather than treating hooks as interchangeable callbacks. ```ts import ky from 'ky'; const measuredApi = ky.extend({ hooks: { beforeRequest: [({request}) => { request.headers.set('x-client', 'web'); }], afterResponse: [({response}) => { console.info('Received response', response.status); }], }, }); async function main(): Promise { const body = await measuredApi.get('https://api.example.com/catalog').json(); console.log(body); } void main(); ``` The request carries the client header; the response hook logs its status, and the call resolves with the parsed body if the response succeeds. Other hook stages include `beforeRetry` and `beforeError`; use `beforeRetry` for work after Ky accepts a retry and `beforeError` to modify an error before Ky throws it. ## Separate retry decisions from retry work Ky retries eligible failures according to its retry limit, allowed methods and status codes, and delay rules. `shouldRetry` customizes whether a retry happens; Ky calls it only after the retry limit and method checks pass. `beforeRetry` runs only after a retry is confirmed, so it is the place to adjust the outgoing retry request or observe the attempt. ```ts import ky, {isHTTPError} from 'ky'; const retryingApi = ky.extend({ retry: { limit: 2, methods: ['get'], shouldRetry: ({error}) => isHTTPError(error) && error.response.status === 503, }, hooks: { beforeRetry: [({retryCount}) => { console.info('Retry attempt', retryCount); }], }, }); async function main(): Promise { const report = await retryingApi.get('https://api.example.com/reports').json(); console.log(report); } void main(); ``` For a GET that receives status 503, the decision permits a retry until the configured limit is reached, and the hook observes each permitted attempt. The call resolves with parsed response data if an attempt succeeds; otherwise it rejects with the final error. Ky also supports a forced retry from `afterResponse` when a response's content calls for another attempt, including a successful status; that retry still respects the configured limit. ## Handle lifecycle errors separately from data validation Use Ky's guards to narrow a caught value by failure type. [`isHTTPError`](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/ky#ishttperror) identifies a response with an error status, [`isNetworkError`](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/ky#isnetworkerror) identifies a network failure, and [`isTimeoutError`](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/ky#istimeouterror) identifies a timeout. [`isKyError`](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/ky#iskyerror) identifies Ky lifecycle errors as a group. ```ts import ky, {isHTTPError, isKyError, isNetworkError, isTimeoutError} from 'ky'; async function main(): Promise { try { const result = await ky.get('https://api.example.com/users/42').json(); console.log(result); } catch (error) { if (isHTTPError(error)) { console.error('HTTP status', error.response.status); } else if (isNetworkError(error)) { console.error('Network failure for', error.request.url); } else if (isTimeoutError(error)) { console.error('Timed out request', error.request.url); } else if (isKyError(error)) { console.error('Other Ky lifecycle error', error.message); } else { throw error; } } } void main(); ``` The guards narrow the caught value so each branch can use the fields for that failure. A [`SchemaValidationError`](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/ky#schemavalidationerror) is different: the HTTP request succeeded, but the supplied Standard Schema rejected the returned data, so it is not a Ky lifecycle error and does not match `isKyError()`. See [Ky errors and validation](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/ky-errors-and-validation) for the consumed response-body detail when handling `HTTPError`, and [validate JSON responses](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/validate-json-responses) for schema validation. ## Related - [Ky quick start](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/ky-quick-start) - [Instances and defaults](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/instances-and-defaults) - [The request lifecycle](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/the-request-lifecycle) - [Retry failed requests](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/retry-failed-requests) - [Ky errors and validation](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/ky-errors-and-validation) - [Validate JSON responses](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/validate-json-responses) # Instances and defaults A Ky instance carries request defaults for a particular use case. Use [`ky`](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/ky#default)'s `create()` method to start an instance with its own defaults, and `extend()` to derive an instance that inherits and modifies its parent's defaults. An instance has the request methods and factory methods described by [`KyInstance`](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/ky#kyinstance). ## How defaults flow Each request combines the instance's defaults with options supplied for that request. Creating from an instance starts fresh; extending it starts from its current defaults. ```mermaid flowchart TD K["ky"] -->|"create(defaults)"| A["Instance A"] A -->|request options| R["Request"] A -->|"extend(overrides)"| B["Instance B: inherited and modified defaults"] B -->|request options| S["Request"] A -->|"create(newDefaults)"| C["Instance C: new defaults only"] ``` ## Extend from parent defaults An `extend()` callback can compute new defaults from the parent defaults. This is useful when a child scope builds on a configured prefix: ```ts import ky from 'ky'; async function main() { const api = ky.create({prefix: 'https://api.example.com/api'}); const usersApi = api.extend(parentOptions => ({ prefix: `${parentOptions.prefix}/users`, })); await usersApi.get('123'); } void main(); ``` The request uses the `https://api.example.com/api/users/123` URL. The callback returns overrides; it does not mutate the parent instance. ## Which factory to use | Call | Defaults used | Choose it when | | --- | --- | --- | | `ky.create(defaultOptions)` | Only the options passed to this call | You need a separate client that does not inherit another instance's defaults. | | `instance.extend(defaultOptions)` | Parent defaults merged with the new options | You need a specialized client based on an existing instance. | | `instance.extend(parentOptions => overrides)` | Parent defaults are available to calculate the returned overrides | The child defaults depend on the parent's configuration. | | [`replaceOption(value)`](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/ky#replaceoption) inside `extend()` | Replaces that merged option value | You need to discard the parent's value for that option. | Use `baseUrl` when relative request inputs should resolve against a base URL. It does not change absolute inputs. `prefix` is applied to string inputs before `baseUrl` resolution; the [`Options`](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/ky-options#options) reference describes both URL options and the rest of the request defaults. # The request lifecycle Ky runs hooks at specific points as a request becomes a response or a retry. The value a hook returns can replace a request or response, skip work, or start another attempt. ## How the hooks fit together Configure the lifecycle through [`Hooks`](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/ky-hooks#hooks) on a [`ky` instance](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/ky#default). ```mermaid flowchart TD A["init hooks: edit options synchronously"] --> B["beforeRequest hooks: edit or replace request"] B -->|"Request continues"| C["Fetch"] B -->|"Response returned"| F["afterResponse hooks"] C --> F F -->|"Response or void"| G{"Non-2xx and throwHttpErrors enabled?"} F -->|"ky.retry()"| H["Retry decision and delay"] G -->|"No"| I["Return response"] G -->|"Yes"| H H -->|"Retry confirmed"| J["beforeRetry hooks"] J --> C H -->|"No retry"| K["beforeError hooks"] K --> L["Throw error"] ``` `init` edits the options before Ky constructs the request. `beforeRequest` runs once, before retry handling begins. If it returns a `Request`, that becomes the outgoing request and the remaining `beforeRequest` hooks still run. If it returns a `Response`, Ky skips the network request and the remaining `beforeRequest` hooks. Returning nothing leaves the request flow unchanged. After a response arrives, `afterResponse` receives a clone, so a hook can inspect its body without consuming the response Ky continues with. Returning a `Response` replaces the current response; returning nothing keeps it. A non-2xx response is checked after these hooks. When HTTP errors are enabled, Ky retries eligible failures before it ultimately throws an [`HTTPError`](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/ky-httperror#httperror). `beforeRetry` runs only after Ky has decided to retry, with a retry count of at least one. A returned `Request` replaces the retry request; a returned `Response` supplies the response instead of another fetch. Returning `ky.stop` stops the retry flow without throwing and leaves no response to read, so body shortcuts such as `.text()` are not suitable for that case. `beforeError` runs as Ky is about to throw an error; return an `Error` to keep or replace the error that reaches the caller. An exception from an `init` hook is synchronous and bypasses `beforeError`. ## Keep hook errors out of retry handling For how `shouldRetry`, `beforeRetry`, and returned requests or responses shape the flow, see [How Ky works](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/how-ky-works). This page adds the error boundary: exceptions from `beforeRequest` and `afterResponse` do not trigger retries, while an `init` exception propagates synchronously and bypasses `beforeError`. For retry eligibility and delay behavior, see [Retry failed requests](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/retry-failed-requests). For handling the errors that leave the lifecycle, see [Ky errors and validation](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/ky-errors-and-validation). # Ky errors and validation Ky separates errors in its HTTP lifecycle from errors raised after a successful response fails schema validation. ## How the errors differ [`KyError`](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/ky#kyerror) is the base class for errors Ky raises during its HTTP lifecycle. A non-2xx response raises [`HTTPError`](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/ky-httperror#httperror) when `throwHttpErrors` is enabled; it includes the response, request, normalized options, and pre-parsed response data. A failed connection raises [`NetworkError`](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/ky#networkerror), which includes the request and original error as `cause`. A request timeout raises [`TimeoutError`](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/ky#timeouterror), which includes the request. A forced retry initiated by an `afterResponse` hook is represented by [`ForceRetryError`](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/ky#forceretryerror). By contrast, [`SchemaValidationError`](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/ky#schemavalidationerror) means Ky received a successful response, parsed its JSON, and the supplied Standard Schema validator rejected that data. It extends `Error`, not `KyError`, and exposes the validator's issues. The lifecycle error family also includes [`ResponseSizeError`](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/ky#responsesizeerror), raised when a response body exceeds `maxResponseSize`; it carries the configured byte limit in `maxResponseSize`. Use [`isResponseSizeError`](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/ky#isresponsesizeerror) to narrow a caught value to that error type. These APIs are unreleased and require the next Ky release; they are not available in the current npm release, Ky 2.1.0. In that release, `ResponseSizeError` is a `KyError`, and `isResponseSizeError(error)` identifies it. ```mermaid flowchart TD A["Ky request"] --> B{"HTTP lifecycle"} B -->|"non-2xx, throwHttpErrors enabled"| C["HTTPError"] B -->|"connection failure"| D["NetworkError"] B -->|"request timeout"| E["TimeoutError"] B -->|"response body exceeds maxResponseSize"| J["ResponseSizeError"] B -->|"forced retry from afterResponse"| F["ForceRetryError"] B -->|"successful response"| G["Parse and validate JSON"] G -->|"schema rejects data"| H["SchemaValidationError"] G -->|"schema accepts data"| I["Validated data"] ``` ## Identify an error at the call site Use Ky's type guards to narrow unknown caught values to lifecycle errors. Check the schema error separately with `instanceof`; the catch block below handles it before the Ky lifecycle guards. The sample calls [`ky`](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/ky#default) and uses Zod as a Standard Schema-compatible validator. Install both packages with `npm install ky zod`, and point the request at an endpoint that returns a JSON user object. ```ts import ky, { SchemaValidationError, isHTTPError, isKyError, isNetworkError, isTimeoutError, } from 'ky'; import {z} from 'zod'; const userSchema = z.object({name: z.string()}); async function loadUser(): Promise { try { const user = await ky.get('https://api.example.com/users/1').json(userSchema); console.log('Validated user:', user); } catch (error) { if (error instanceof SchemaValidationError) { console.error('Response data did not match the schema:', error.issues); } else if (isHTTPError(error)) { console.error('HTTP status:', error.response.status); console.error('Error response body:', error.data); } else if (isNetworkError(error)) { console.error('Network failure for:', error.request.url); } else if (isTimeoutError(error)) { console.error('Timed out request:', error.request.url); } else if (isKyError(error)) { console.error('Other Ky lifecycle error:', error.message); } else { console.error('Other error:', error); } } } void loadUser(); ``` If the response contains a user with a string `name`, the call returns and logs the validated user. A non-2xx response enters the `HTTPError` branch, a connection failure or timeout enters its matching branch, and a successful response with invalid data enters the schema-error branch. The guards narrow `error` so each branch can read that error type's properties. Ky detects network errors with runtime-specific heuristics, so an unrecognized runtime may not wrap a connection failure in `NetworkError`. When Ky fills `HTTPError.data`, it consumes the response body. Read the parsed error body from `error.data`; calling `error.response.json()` or another body method afterward does not work. The response remains available for status and headers, and `data` can be `undefined` if the body is empty, unreadable, or cannot be parsed. For the general lifecycle guards and hook timing, see [How Ky works](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/how-ky-works). For the additional forced-retry case, [`isForceRetryError`](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/ky#isforceretryerror) identifies the `ForceRetryError` signal in `beforeRetry` and `beforeError` hooks. ## Related - [How Ky works](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/how-ky-works) - [The request lifecycle](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/the-request-lifecycle) - [Validate JSON responses](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/validate-json-responses) - [Retry failed requests](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/retry-failed-requests) # Add request headers Use a per-request `headers` option for one-off values, or add a `beforeRequest` hook to a [`ky`](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/ky#default) instance to attach shared headers to its requests. ## When to use each approach Set headers on the request when a value applies only to that call. Put a header in an instance's request hook when the same value belongs on every request made through that instance, such as an application identifier. ## Set headers for one request Pass a plain object as `headers` in the request options. The header is sent with this request; other calls made through `ky` do not inherit it. ```ts import ky from 'ky'; async function loadProfile(): Promise { const response = await ky.get('https://api.example.com/v1/profile', { headers: { 'x-request-id': 'profile-view-42', }, }); const body = await response.text(); console.log(body); } void loadProfile(); ``` The request carries `x-request-id: profile-view-42`. In a browser, inspect the request's Headers in the Network panel to see the header that was sent. The call returns a response promise; here, `.text()` reads its response body. ## Add a shared header with a request hook Create an instance with `extend()` and use its `beforeRequest` hook to modify the outgoing request. The hook receives the request, so set the header on `request.headers`. ```ts import ky from 'ky'; const api = ky.extend({ hooks: { beforeRequest: [ ({request}) => { request.headers.set('x-client-id', 'web-dashboard'); }, ], }, }); async function loadProfile(): Promise { const response = await api.get('https://api.example.com/v1/profile'); const body = await response.text(); console.log(body); } void loadProfile(); ``` Requests made through `api` carry `x-client-id: web-dashboard`; calls through the default `ky` instance do not use this hook. The hook runs once before retry handling begins, and the response promise still gives you the response to read. ## Options that matter | Option | Type | Default | What it does | | --- | --- | --- | --- | | [`Options`](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/ky-options#options) `headers` | `Options['headers']` | — | Sets HTTP headers for an individual request or as an instance default. | | `hooks.beforeRequest` | [`Hooks`](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/ky-hooks#hooks)`['beforeRequest']` | `[]` | Runs before the request is sent; use its `request` argument to set shared headers. | ## Pitfalls - A `beforeRequest` hook runs once with `retryCount` equal to `0`; it does not run again for a retry. Use the retry lifecycle when a header needs to change before a retry. - An error thrown by a `beforeRequest` hook is fatal and does not trigger Ky's retry logic. - The `json` option sets `Content-Type` to `application/json` unless your request `headers` option sets `Content-Type` itself. ## Related - [Instances and defaults](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/instances-and-defaults) - [The request lifecycle](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/the-request-lifecycle) - [Send and read JSON](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/send-and-read-json) - [Ky options](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/ky-options) # Resolve API URLs Use `baseUrl` for standard URL resolution; use `prefix` when a leading slash in a request path must stay under the configured API path. ## Choose how relative paths resolve Set defaults on a [`ky`](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/ky#default) instance with `ky.extend()`, then keep request paths relative: ```ts import ky from 'ky'; const run = async (): Promise => { const baseUrlApi = ky.extend({ baseUrl: 'https://jsonplaceholder.typicode.com/users/', }); const prefixApi = ky.extend({ prefix: 'https://jsonplaceholder.typicode.com/users', }); const baseUrlResponse = await baseUrlApi.get('1'); const prefixResponse = await prefixApi.get('/1'); console.log(baseUrlResponse.url); console.log(prefixResponse.url); }; void run(); ``` Both requests target `https://jsonplaceholder.typicode.com/users/1`. `baseUrl` resolves `'1'` relative to the trailing-slash path. `prefix` joins its path with `'/1'`, trimming the boundary slashes before URL resolution. Ky instances created with `extend()` inherit the parent instance's defaults. ## Pick the option that matches your paths | Option | Type | Default | What it does | | --- | --- | --- | --- | | `baseUrl` | `URL \| string \| undefined` | No value set | Resolves a relative input as a URL reference. An input beginning with `/` replaces the base URL's path, so a base of `https://example.com/api/` and input `/users` resolves to `https://example.com/users`. | | `prefix` | `URL \| string \| undefined` | No value set | Joins the prefix and string input before resolving the resulting URL. Leading slashes on the input are trimmed at the join boundary, so `/users` stays under an `/api/` prefix. | In most cases, choose `baseUrl`: it follows standard URL resolution. Include a trailing slash in a base URL that has a path, such as `https://example.com/api/`, so a page-relative input like `'users'` extends that path. Choose `prefix` when your callers use origin-relative paths such as `'/users'` but you want those paths appended under the API prefix. An absolute input bypasses `baseUrl`. `prefix` applies only when the input is a string; when the input is a `Request`, both `baseUrl` and `prefix` are ignored. ## Related - [Ky quick start](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/ky-quick-start) - [Instances and defaults](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/instances-and-defaults) # Retry failed requests Configure retry limits, eligible methods and statuses, and the delay between attempts; add a custom decision when the default retry rules do not match your API. Use retries for requests that can safely be repeated after a transient failure. ## Set retry limits and eligibility Set retry defaults on a [`ky` instance](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/ky#default) when several calls share the same policy, or pass `retry` on an individual request. The value can be a number, which sets only the retry limit, or a [`RetryOptions`](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/ky-retryoptions#retryoptions) object. 1. Create an instance with the retry policy and make a request: ```ts import ky from 'ky'; const api = ky.extend({ retry: { limit: 3, methods: ['get'], statusCodes: [429, 503], afterStatusCodes: [429, 503], maxRetryAfter: 30_000, backoffLimit: 2_000, jitter: true, }, }); const run = async (): Promise => { const report = await api.get('https://api.example.com/reports').json(); console.log(report); }; void run(); ``` The instance makes GET requests eligible for up to three retries on the selected statuses. For those statuses, Ky uses retry timing headers when available and caps a header-provided delay at 30 seconds. Otherwise it uses the configured backoff and jitter. If an attempt succeeds, `.json()` resolves with the parsed response body; if retries are exhausted, the request rejects with its error. ## Customize the retry decision Use `shouldRetry` to decide whether a failed attempt merits another try. Ky calls it only after the retry limit and method checks pass. Return `true` to retry, `false` to stop, or `undefined` (or no value) to keep Ky's default decision. A `true` result bypasses the default timeout and status-code checks, but not the retry limit or method checks. 1. Add a decision for rate limits while leaving other failures to the default rules: ```ts import ky, {HTTPError} from 'ky'; const api = ky.extend({ retry: { limit: 3, methods: ['get'], statusCodes: [429, 503], shouldRetry: ({error, retryCount}) => { if (error instanceof HTTPError) { const status = error.response.status; if (status === 429) { return retryCount <= 2; } if (status >= 400 && status < 500) { return false; } } return undefined; }, }, }); const run = async (): Promise => { const report = await api.get('https://api.example.com/reports').json(); console.log(report); }; void run(); ``` `retryCount` starts at `1` for the first retry. This policy allows at most two retries for `429`, rejects other `4xx` failures without retrying, and delegates other failures to Ky's default checks. For example, a `503` remains eligible under the configured `statusCodes`; recognized network errors also use the default behavior for retriable methods. `shouldRetry` decides whether Ky retries. The `beforeRetry` hook runs only after Ky confirms the retry and is where you modify the request. ## Retry options The defaults below apply when you provide an object and omit a field. Set `limit` to `0` to disable retries. A numeric `retry` value is shorthand for `limit`; the other defaults remain in effect. | Option | Type | Default | What it does | | --- | --- | --- | --- | | `limit` | `number` | `2` | Sets the number of retries. It must be a finite, non-negative integer. | | `methods` | `readonly HttpMethod[]` | `['get', 'put', 'head', 'delete', 'options', 'trace', 'query']` | Restricts retries to these HTTP methods. | | `statusCodes` | `readonly number[]` | `[408, 413, 429, 500, 502, 503, 504]` | Lists HTTP response statuses eligible for retry. `413` is retried only when the response has a retry timing header. | | `afterStatusCodes` | `readonly number[]` | `[413, 429, 503]` | Selects statuses that use retry timing headers. Each status must also appear in `statusCodes`. | | `maxRetryAfter` | `number` | `Infinity` | Caps a delay obtained from a retry timing header. | | `backoffLimit` | `number` | `Infinity` | Caps each calculated retry delay in milliseconds. | | `delay` | `(attemptCount: number) => number` | `attemptCount => 0.3 * (2 ** (attemptCount - 1)) * 1000` | Calculates a delay in milliseconds; the attempt count starts at `1`. | | `jitter` | `boolean \| ((delay: number) => number)` | `undefined` | Adds random variation to calculated retry delays. `true` uses full jitter. Jitter does not change a delay supplied by a server timing header. | | `retryOnTimeout` | `boolean` | `false` | Enables retries when a request times out before a response arrives. A timeout while a shortcut method reads a received response body is not retried. | | `shouldRetry` | `(state: ShouldRetryState) => boolean \| void \| Promise` | `undefined` | Applies a custom retry decision after limit and method checks pass. | ## Pitfalls - With retries enabled, Ky buffers a streaming request body in memory so it can replay it. For a large streaming upload that does not need retries, set `retry: {limit: 0}`. - A retry request returned from `beforeRetry` or supplied to `ky.retry({request})` is used as-is and is not sanitized. If it targets another origin, remove credentials you do not want forwarded. - Ky automatically retries recognized network errors for retriable methods. Other unrecognized errors are not retried by default; use `shouldRetry` when your runtime or application needs a different decision. - When Ky populates `HTTPError.data`, it consumes the response body. Do not then call `error.response.json()` or another body method; use `error.data` instead. See [Ky errors and validation](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/ky-errors-and-validation). ## Related - [How Ky works](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/how-ky-works) - [The request lifecycle](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/the-request-lifecycle) - [Ky errors and validation](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/ky-errors-and-validation) # Send and read JSON Use [`ky`](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/ky#default) when you want to send a JSON request body and read the response as a typed value. The `json` option serializes the request body, and `.json()` parses the response body and gives the result the TypeScript type `T`. ## Send a request and read its response 1. Pass the value to send in the `json` option; see [Add request headers](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/add-request-headers) for how Ky serializes it and sets `Content-Type`. 2. Call `.json()` on the request to get a promise for the parsed response body, typed to the response shape your endpoint returns. ```ts import ky from 'ky'; const main = async (): Promise => { const createdUser = await ky.post('https://api.example.com/users', { json: {name: 'Ada'}, }).json<{id: number; name: string}>(); console.log(`Created ${createdUser.name} (${createdUser.id})`); }; void main(); ``` When the endpoint returns JSON with an `id` and `name`, `createdUser` has those typed fields and the log displays them. The type argument guides TypeScript; it does not validate the response at runtime. Keep it aligned with the endpoint's response contract. ## Options that matter | Option or method | Type | Default | What it does | | --- | --- | --- | --- | | `json` | `unknown` | Not specified | Supplies this request's JSON payload; see [Add request headers](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/add-request-headers) for serialization and content-type behavior. | | `.json()` | `(schema?: undefined): Promise` | `T` defaults to `unknown` | Types the parsed response as the endpoint's response shape; it does not validate that shape at runtime. | The body method also sets an appropriate `Accept` header. It throws an [`HTTPError`](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/ky-httperror#httperror) for a non-2xx response when HTTP errors are enabled, and `.json()` throws if the body is empty because it cannot be parsed. ## Pitfalls - `ky.stop` makes the request resolve with `undefined`, so there is no response to inspect and body shortcuts such as `.json()` are incompatible with it. Throw from `beforeRetry` instead when the caller needs an error rather than a missing response. - The type argument on `.json()` is not runtime validation. If the response needs validation, use the approach in [Validate JSON responses](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/validate-json-responses). ## Related - [Ky quick start](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/ky-quick-start) - [Add request headers](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/add-request-headers) - [Ky errors and validation](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/ky-errors-and-validation) # Set request timeouts Set a per-attempt limit and an overall limit when a request must not wait indefinitely. ## When to use request timeouts Use `timeout` to cap the time Ky spends waiting for a response on each attempt. Set `totalTimeout` when retries and their delays must also fit within one overall time budget. Ky is an HTTP client based on the Fetch API, so make the request with [`ky`](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/ky#default) and pass these as request options. ## Set both limits on a request Pass both limits in the request options. When the response arrives and its body is read within the limits, `.json()` resolves with the parsed value and the sample logs it. ```ts import ky from 'ky'; async function loadReports(): Promise { const reports = await ky.get('https://api.example.com/reports', { timeout: 5_000, totalTimeout: 30_000, }).json(); console.log(reports); } void loadReports(); ``` ## Timeout options | Option | Type | Default | What it does | | --- | --- | --- | --- | | `timeout` | `number \| false` | `10000` | Limits each attempt while waiting for a response. Ky's shortcut body methods, such as `.json()`, also use it as a separate limit for reading the response body. `false` disables the per-attempt limit. | | `totalTimeout` | `number \| false` | `false` | Limits the whole operation, including retries and retry delays. If the limit is exceeded, Ky throws a [`TimeoutError`](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/ky#timeouterror). `false` or omitting the option disables this overall limit. | | `retry.retryOnTimeout` | `boolean` | `false` | Enables retries when a timeout happens before Ky receives a response. | Both numeric timeout values must not exceed `2147483647` milliseconds. `beforeError` hooks run after Ky produces an error and are not bounded by `totalTimeout`. ## Related - [Retry failed requests](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/retry-failed-requests) explains retry rules and retry timing. - [Ky errors and validation](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/ky-errors-and-validation) covers Ky's error types and handling patterns. # Validate JSON responses Use this when you need Ky to parse a JSON response and verify its shape before your code uses it. ## Validate with a Standard Schema Pass a Standard Schema-compatible validator to `.json()`. The returned promise resolves to the validator's output type; if validation fails, Ky rejects it with a `SchemaValidationError` that carries the validator's issues. 1. Install Ky and a compatible validator. Ky's documentation uses Zod 3.24 or later. ```bash npm install ky zod ``` 2. Define the schema and pass it to [`ky`](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/ky#default)'s `.json()` method. This browser example requests your same-origin `/api/user` endpoint; with a response matching the schema, it logs the validated user object. ```ts import ky, {HTTPError, SchemaValidationError, isKyError} from 'ky'; import {z} from 'zod'; const userSchema = z.object({name: z.string()}); const loadUser = async (): Promise => { try { const user = await ky('/api/user').json(userSchema); console.log(user); } catch (error: unknown) { if (error instanceof SchemaValidationError) { console.error('The response did not match the schema:', error.issues); } else if (isKyError(error)) { if (error instanceof HTTPError) { console.error('The request returned an unsuccessful status:', error.response.status); } else { console.error('Ky request error:', error.message); } } else { throw error; } } }; void loadUser(); ``` The schema check runs after Ky has received and parsed the JSON. A matching response resolves with the validated value. A mismatch enters the first error branch with the validator's issues; an unsuccessful HTTP response enters the Ky error handling instead. ## Schema argument | Argument | Type | Default | Effect | | --- | --- | --- | --- | | `schema` | Standard Schema-compatible validator | No schema | Validates the parsed JSON and types the resolved value as the schema's output. Without a schema, `.json()` parses JSON without schema validation. | ## Pitfall: schema errors are not Ky errors For the error-type distinctions, see [How Ky works](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/how-ky-works); this page focuses on reporting the validator's issues when response data fails the schema. ## Related - [Send and read JSON](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/send-and-read-json) - [Ky errors and validation](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/ky-errors-and-validation) # ky Tiny and elegant HTTP client based on the Fetch API ## Install ```bash npm install ky ``` ## On their own pages - [`Hooks`](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/ky-hooks) - [`HTTPError`](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/ky-httperror): Error thrown when the response has a non-2xx status code and `throwHttpErrors` is enabled. - [`KyRequest`](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/ky-kyrequest) - [`NormalizedOptions`](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/ky-normalizedoptions): Normalized options passed to the `fetch` call and hooks. - [`Options`](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/ky-options): Options are the same as `window.fetch`, except for the KyOptions - [`ResponsePromise`](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/developer/ky-responsepromise) - [`RetryOptions`](https://bench-ky-6l.atloria.app/p/bench-ky-6l-QSDF7DxJ6h/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. |