Skip to content
D
Documentation

Error handling and validation

how-to
3 min readUpdated

Use ky to separate HTTP failures, transport failures, timeouts, response-size limits, and response-schema failures. This page covers the request lifecycle, HTTP error data, bounded response reads, and Standard Schema validation.

When to use this

Use this pattern when a request needs different handling for a non-2xx response, a missing connection, a timeout, a forced retry, or a response that does not match the shape your code expects. A non-2xx response with throwHttpErrors enabled throws HTTPError; a schema rejection throws SchemaValidationError after the request succeeds.

Handle lifecycle failures in one place

Put a beforeError hook on an instance when several requests need the same classification or message handling. The hook receives the current request, normalized options, error, and retry count, and returns the Error that Ky throws.

For the error taxonomy and the type guards isKyError, isHTTPError, isNetworkError, isTimeoutError, and isForceRetryError, see Requests and responses and How Ky works. This page applies that model in a shared beforeError hook.

ts
import ky, {
	isForceRetryError,
	isHTTPError,
	isKyError,
	isNetworkError,
	isTimeoutError,
} from 'ky';

const api = ky.extend({
	hooks: {
		beforeError: [({error, request, retryCount}) => {
			if (isHTTPError(error)) {
				const data = error.data;
				if (typeof data === 'object' && data !== null && 'message' in data) {
					error.message = `${String(data.message)} (${error.response.status})`;
				}
			} else if (isNetworkError(error)) {
				console.error(`No response from ${error.request.url}`);
			} else if (isTimeoutError(error)) {
				console.error(`Timed out: ${error.request.url}`);
			} else if (isForceRetryError(error)) {
				console.error(`Retry stopped: ${error.code ?? 'unknown reason'}`);
			} else if (isKyError(error)) {
				console.error(`Ky request failed on attempt ${retryCount + 1}: ${request.url}`);
			}

			return error;
		}],
	},
});

const loadUser = async (): Promise<void> => {
	try {
		await api.get('https://api.example.com/user').json();
	} catch (error) {
		console.error(error);
	}
};

void loadUser();

The call returns parsed JSON on success. On failure, the hook classifies the error before the catch block receives it. A network failure has no response; use its request instead. isKyError() covers Ky's HTTP lifecycle errors, but not SchemaValidationError.

Read an HTTP error body

Requests and responses covers the standard HTTPError fields. In an application error handler, use the pre-parsed payload to turn a structured server error into a user-facing message:

ts
import ky, {isHTTPError} from 'ky';

const readAccount = async (): Promise<void> => {
	try {
		await ky.get('https://api.example.com/account').json();
	} catch (error) {
		if (isHTTPError(error)) {
			const data = error.data;
			const message = typeof data === 'object' && data !== null && 'message' in data
				? String(data.message)
				: 'The account request failed';
			console.error(`${message} (HTTP ${error.response.status})`);
		} else {
			throw error;
		}
	}
};

void readAccount();

For how Ky parses and consumes an HTTP error body, see Requests and responses. The sample uses the resulting data value to choose a message while retaining response.status for context.

Limit response size

maxResponseSize limits bytes read from the decompressed response stream. Set it to a non-negative safe integer; 0 permits only an empty body. Exceeding the limit cancels the stream and raises ResponseSizeError without an automatic retry. The limit bounds body bytes, not all memory used by parsing, buffering, or concurrent requests.

maxResponseSize and isResponseSizeError are in the repository's next release, not in Ky 2.1.0, the current npm release. Use the following with a build that contains that API:

ts
import ky, {isResponseSizeError} from 'ky';

const readArchive = async (): Promise<void> => {
	try {
		await ky('https://api.example.com/archive', {
			maxResponseSize: 1024 * 1024,
		}).json();
	} catch (error) {
		if (isResponseSizeError(error)) {
			console.error(`Response exceeded ${error.maxResponseSize} bytes`);
		} else {
			throw error;
		}
	}
};

void readArchive();

The request can resolve before a later body read exceeds the limit when you use await ky(url) without a body shortcut; consume the body to observe the limit.

Validate the JSON response

For the .json(schema) flow and the distinction between SchemaValidationError and Ky lifecycle errors, see How Ky works. This page adds a reporting pattern that turns each validation issue into a path-and-message entry.

ts
import ky, {SchemaValidationError} from 'ky';
import {z} from 'zod';

const userSchema = z.object({
	name: z.string(),
});

const readUser = async (): Promise<void> => {
	try {
		const user = await ky('https://api.example.com/user').json(userSchema);
		console.log(`Loaded ${user.name}`);
	} catch (error) {
		if (error instanceof SchemaValidationError) {
			for (const issue of error.issues) {
				const path = issue.path?.map(part => typeof part === 'object' ? String(part.key) : String(part)).join('.') ?? '<root>';
				console.error(`${path}: ${issue.message}`);
			}
		} else {
			console.error('Request failed', error);
		}
	}
};

void readUser();

The success path receives a value with name: string; a rejected response produces one log entry per issue, including its property path. For the boundary between schema validation and Ky lifecycle errors, see How Ky works.

Options that affect failures

OptionTypeDefaultWhat it does
throwHttpErrorsbooleantrueThrows an HTTPError for a non-2xx response after redirects.
timeoutnumber | false10000Sets the per-attempt timeout for receiving a response and, for shortcut methods, reading the body.
totalTimeoutnumber | falsefalseSets an overall limit for the operation, including retries and delays.
maxResponseSizenumberInfinityLimits decompressed response-body bytes; available in the next release, not Ky 2.1.0.

beforeError runs after an error exists and is not bounded by totalTimeout. Use shouldRetry to change retry decisions; beforeRetry runs only after Ky has selected a retry.

Pitfall

For the response-body consumption rule, see Requests and responses. In this page's handler, keep the pre-parsed error.data for the message and use error.response only for metadata such as the status.

Was this page helpful?

Error handling and validation — ky · GPT-5.6 Luna