Ky is a Fetch-based HTTP client with a shorter request interface, reusable defaults, lifecycle hooks, configurable retries, and separate response and error types.
Instances hold defaults
The KyInstance type describes the callable client and its method shortcuts. Call ky.create() for an independent client. Call ky.extend() when a client should inherit a parent’s defaults and add or override values. Use replaceOption when an inherited option must be replaced rather than merged.
tsimport ky, {replaceOption} from 'ky';
const api = ky.create({
baseUrl: 'https://api.example.com/',
headers: {'x-client': 'admin'},
});
const users = api.extend({
baseUrl: 'https://api.example.com/users/',
});
const isolated = ky.create({
headers: replaceOption({'x-client': 'reports'}),
});
const main = async (): Promise<void> => {
const response = await users.get('42');
console.log(response.url);
console.log(isolated);
};
void main();
extend() deep-merges options by default: hooks append, headers merge, and search parameters accumulate. create() does not inherit the parent’s defaults. The URL printed by the sample is the user client’s URL, and the request uses the inherited header.
Hooks form the lifecycle
Hooks run at defined points around the request:
initcan synchronously modify mutable options before Ky constructs the request.beforeRequestcan modify or replace the outgoing request, or return a response to avoid the HTTP request.beforeRetryruns only after Ky has selected a retry.afterResponsecan modify a response or request another attempt.beforeErrorcan modify the error before it reaches the caller.
tsimport ky from 'ky';
const api = ky.extend({
hooks: {
init: [options => {
options.searchParams = {source: 'dashboard'};
}],
beforeRequest: [({request}) => {
request.headers.set('Authorization', 'Bearer <token>');
}],
afterResponse: [({response}) => {
return response;
}],
beforeError: [({error}) => {
error.message = `Dashboard request failed: ${error.message}`;
return error;
}],
},
});
const main = async (): Promise<void> => {
const response = await api.get('https://api.example.com/users');
console.log(response.status);
};
void main();
The request carries the source search parameter and authorization header. The response remains available to the caller, while an error receives the added message. Keep authentication and request shaping in hooks rather than duplicating them at every call site.
Retries are part of the lifecycle
Retry decisions use the configured retry options together with the request method, retry limit, status codes, network errors, timeouts, and server timing headers. shouldRetry can override the default retry checks after the method and limit checks pass. beforeRetry changes the request only after that decision. An afterResponse hook can return ky.retry() to force an attempt based on response content; that attempt still respects the retry limit and is visible in beforeRetry as a ForceRetryError, which isForceRetryError can identify.
tsimport ky, {isForceRetryError} from 'ky';
const api = ky.extend({
retry: {
limit: 2,
retryOnTimeout: true,
},
hooks: {
afterResponse: [async ({response}) => {
const data = await response.clone().json<{temporary?: boolean}>();
if (data.temporary === true) {
return ky.retry();
}
}],
beforeRetry: [({error, retryCount}) => {
if (isForceRetryError(error)) {
console.log(`Forced retry #${retryCount}`);
}
}],
},
});
const main = async (): Promise<void> => {
const response = await api.get('https://api.example.com/status');
console.log(response.status);
};
void main();
The sample logs the returned status. The clone() keeps the response available for later processing while the hook reads its body.
Response data and failures stay distinct
Ky separates an HTTP failure from a successful response whose data fails your schema. KyError is the base for Ky lifecycle errors, including NetworkError, TimeoutError, HTTPError, and ForceRetryError. Use isKyError, isHTTPError, isNetworkError, and isTimeoutError to narrow caught values. SchemaValidationError is separate: the request succeeded, but the Standard Schema validator rejected the body, so isKyError() does not match it.
tsimport ky, {
isHTTPError,
isKyError,
isNetworkError,
isTimeoutError,
} from 'ky';
const main = async (): Promise<void> => {
try {
await ky.get('https://api.example.com/users/1').json();
} catch (error) {
if (isHTTPError(error)) {
console.log(error.response.status, error.data);
} else if (isNetworkError(error)) {
console.log(error.request.url);
} else if (isTimeoutError(error)) {
console.log(error.request.url);
} else if (isKyError(error)) {
console.log(error.message);
} else {
throw error;
}
}
};
void main();
An HTTPError contains the failed response, request, normalized options, and pre-parsed data. Ky consumes the response body while populating data, so use error.data rather than calling a body method on error.response; see Error handling and validation. Network failures have no response and use NetworkError instead.
For schema validation, pass a Standard Schema-compatible validator to .json(schema). A failed validation throws SchemaValidationError with its issues; it does not turn a successful HTTP response into an HTTP error. Use JSON requests and responses for the validation pattern.
These boundaries are the model to carry into the rest of the API: a KyResponse represents the received response, hooks control lifecycle points, retry settings control additional attempts, and error classes identify whether the failure came from HTTP, transport, timing, retry control, or data validation. Continue with Requests and responses, Instances and defaults, and Ky’s error model.
Was this page helpful?