# 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<void> {
  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<void> {
  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<void> {
  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<void> {
  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<void> {
  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)
