Skip to content
D
Documentation

Request lifecycle

concept
2 min readUpdated

The request lifecycle is the sequence in which ky 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 for request replacement and network bypass.

By default, a non-2xx response becomes an 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; 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 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 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 for ordinary and forced retry eligibility.

See Retry failed requests for policy configuration and Authenticate requests for changing credentials at these stages.

Returning a response versus consuming its body

A Ky call returns a ResponsePromise: await it to get a 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 for JSON options and shortcuts, and 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 for timeout budgets and HTTP-error handling.

For error narrowing and recovery, see Handle request errors. For how defaults compose before this lifecycle starts, see Instances and defaults.

Was this page helpful?