# Request lifecycle

The request lifecycle is the sequence in which [`ky`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky#default) turns Fetch inputs and options into a request, handles attempts, and returns a response for you to consume.

Ky builds on the Fetch API: pass a string, `URL`, or `Request`, with Fetch options plus Ky's additional options. Hooks modify specific stages of this sequence, not an interchangeable interception point.

## From options to a response

Ky merges instance defaults with the options for the call, runs synchronous `init` hooks, then constructs the request. URL resolution, headers, search parameters, and an outgoing `json` body take effect before `beforeRequest` runs.

```mermaid
flowchart TD
    A["Input and merged options"] --> B["init: modify options"]
    B --> C["Construct Request"]
    C --> D["beforeRequest: initial request only"]
    D --> E["Fetch attempt"]
    E -->|Response| F["afterResponse: read or replace a clone"]
    E -->|Network failure or timeout| G["Retry decision"]
    F -->|Unsuccessful HTTP status| G
    F -->|"ky.retry()"| G
    G -->|Retry allowed| H["Delay, then beforeRetry"]
    H --> E
    G -->|No retry| I["beforeError, then reject"]
    F -->|Accepted response| J["Resolve with Response"]
    J --> K["Caller reads body or uses a Ky shortcut"]
```

The diagram shows the normal network path; returning a `Response` from `beforeRequest` stops the remaining hooks at that stage, whereas returning a `Request` lets them continue with the replacement—see [How Ky works](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/how-ky-works) for request replacement and network bypass.

By default, a non-2xx response becomes an [`HTTPError`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky-httperror#httperror) after `afterResponse` runs. This order lets an `afterResponse` hook replace the response before Ky checks its status.

## Hooks across attempts

Use `init` for mutable options, `beforeRequest` for the initial outgoing request, and `beforeRetry` for a retry Ky has already approved. Later hooks receive the request and read-only [`NormalizedOptions`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky-normalizedoptions#normalizedoptions); inspect `request` for the actual request state if a hook replaces it, because `options` remains Ky's normalized snapshot.

This sample runs in a browser or Node.js with Fetch support. It requests users with a search parameter and a header, and traces the stages in execution order, with `retryCount` distinguishing the initial attempt from retries. Use the trace to locate where your request stops or starts another attempt; see [How Ky works](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/how-ky-works) for each hook's role.

```ts
import ky from 'ky';

const api = ky.extend({
    baseUrl: 'https://api.example.com/',
    retry: 2,
    hooks: {
        init: [options => {
            options.searchParams = {active: true};
            console.log('init');
        }],
        beforeRequest: [({request, retryCount}) => {
            request.headers.set('x-client', 'lifecycle-example');
            console.log('beforeRequest', retryCount, request.url);
        }],
        beforeRetry: [({request, error, retryCount}) => {
            console.log('beforeRetry', retryCount, request.url, error.message);
        }],
        afterResponse: [({response, retryCount}) => {
            console.log('afterResponse', retryCount, response.status);
        }],
        beforeError: [({error, retryCount}) => {
            console.error('beforeError', retryCount, error.message);
            return error;
        }],
    },
});

async function loadUsers(): Promise<void> {
    try {
        const users = await api.get('users').json();
        console.log(users);
    } catch (error) {
        console.error('Request failed', error);
    }
}

void loadUsers();
```

Two boundaries affect this trace: an error thrown in `init` bypasses `beforeError`, and returning either a `Request` or a `Response` from `beforeRetry` skips its remaining hooks—see [How Ky works](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/how-ky-works) for the hook contracts.

Read the retry loop as a decision followed by a delay and only then a confirmed-retry hook; see [How Ky works](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/how-ky-works) for ordinary and forced retry eligibility.

See [Retry failed requests](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/retry-failed-requests) for policy configuration and [Authenticate requests](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/authenticate-requests) for changing credentials at these stages.

## Returning a response versus consuming its body

A Ky call returns a [`ResponsePromise`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky-responsepromise#responsepromise): await it to get a [`KyResponse`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky#kyresponse), or call a body shortcut on it to request and consume the response in one expression.

These are two separate requests. The first returns a response whose headers and body you handle yourself. The second sends JSON and returns parsed response data rather than a response object.

```ts
import ky from 'ky';

async function requestUsers(): Promise<void> {
    const response = await ky.get('https://api.example.com/users');
    console.log(response.status, response.headers.get('content-type'));
    const users = await response.json();
    console.log(users);

    const createdUser = await ky.post('https://api.example.com/users', {
        json: {name: 'Ada'},
    }).json();
    console.log(createdUser);
}

void requestUsers().catch(error => {
    console.error('Request failed', error);
});
```

Here, the first call hands body consumption to your code after the request resolves, while the second keeps it inside the shortcut operation; see the [quick start](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/quick-start) for JSON options and shortcuts, and [Send and validate JSON](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/send-and-validate-json) for response validation.

The distinction matters for timeouts and errors:

Once the response arrives, a shortcut body-read network failure or timeout reaches `beforeError` without re-entering the retry loop, whereas a body read on the returned response is outside that lifecycle; see [How Ky works](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/how-ky-works) for timeout budgets and HTTP-error handling.

For error narrowing and recovery, see [Handle request errors](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/handle-request-errors). For how defaults compose before this lifecycle starts, see [Instances and defaults](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/instances-and-defaults).
