Skip to content
D
Documentation

Ky's error model

concept
3 min readUpdated

Ky separates failures in the HTTP lifecycle from failures in the data you validate after a response arrives. Use the error type to decide whether to inspect a response, retry a request, report a timeout, or fix the schema.

Call the default export ky for requests and method shortcuts such as ky.get().

One hierarchy, one deliberate exception

The KyError hierarchy and its deliberate SchemaValidationError exception are covered in How Ky works; this page applies that distinction when choosing an error branch.

mermaid
flowchart TD
    Request["Ky request"] --> Response{"Response received?"}
    Response -->|"no"| Network["NetworkError"]
    Response -->|"yes"| Status{"2xx status?"}
    Status -->|"no"| HTTP["HTTPError"]
    Status -->|"yes"| Body{"Body operation succeeds?"}
    Body -->|"timeout"| Timeout["TimeoutError"]
    Body -->|"too large"| Size["ResponseSizeError"]
    Body -->|"JSON schema rejects"| Schema["SchemaValidationError"]
    Body -->|"yes"| Value["Validated or typed value"]

For the broad cross-realm isKyError check and its schema-validation boundary, see How Ky works.

HTTP failures

An HTTPError means Ky received a non-2xx response while throwHttpErrors is enabled. Inspect error.response.status, error.response.headers, and error.data. Ky populates data before error hooks run and parses JSON responses with parseJson when that option is set, or with JSON.parse otherwise. For other content types, data is text.

Ky consumes the response body while populating error.data. Do not call error.response.json() or another body method afterwards; use error.data. The response remains useful for status and headers. See error handling and validation for the handling pattern used by this guide.

Use isHTTPError to narrow an unknown catch value before reading the response.

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

async function loadStatus(): Promise<void> {
	try {
		await ky.get('https://api.example.com/status').json();
	} catch (error) {
		if (isHTTPError(error)) {
			console.error(error.response.status, error.data);
			return;
		}

		throw error;
	}
}

void loadStatus();

The call either gives you the parsed body shortcut or enters the HTTP branch with the status and pre-parsed error data. A network failure does not enter this branch because it has no response to expose.

Network and timeout failures

NetworkError means the request failed at the network layer, such as through DNS failure, connection refusal, or an offline runtime. It carries the request, and the original runtime error is its cause. Network errors are automatically retried for retriable methods. A connection that drops while .json() or another shortcut reads an already received response is also wrapped as NetworkError, but Ky does not retry that body-read failure because the response already arrived. Runtime-specific detection can miss an unfamiliar error shape; use the shouldRetry option for that case.

TimeoutError means the request timed out and also carries the request. timeout applies to an attempt, while totalTimeout limits the whole operation across attempts and delays. Both are distinct from a server response with a non-2xx status.

Use isNetworkError and isTimeoutError to narrow these two cases.

ts
import ky, {isNetworkError, isTimeoutError} from 'ky';

async function requestWithDiagnostics(): Promise<void> {
	try {
		await ky.get('https://api.example.com/report', {
			timeout: 1_000,
			totalTimeout: 5_000,
		}).json();
	} catch (error) {
		if (isTimeoutError(error)) {
			console.error('Timed out:', error.request.url);
			return;
		}

		if (isNetworkError(error)) {
			console.error('Network failure:', error.request.url);
			return;
		}

		throw error;
	}
}

void requestWithDiagnostics();

The timeout branch reports an attempt or overall time limit. The network branch reports a request that did not produce a usable response. A network failure while a shortcut reads an already received response is not retried; use shouldRetry when your runtime needs a custom retry decision.

Response-size failures

ResponseSizeError is the shipped error for a response body that exceeds maxResponseSize. It carries the request and the configured byte limit. The limit counts bytes from the decompressed response stream as Ky consumes it, and exceeding it does not automatically retry.

ResponseSizeError, maxResponseSize, and isResponseSizeError are not in the current Ky 2.1.0 npm release. Use them only after installing a release that exports them; do not add this branch to an application pinned to 2.1.0.

Forced retries

Use the forced-retry branch when response content—not a transport failure—requires another attempt. The lifecycle and hook pattern are covered in How Ky works; this page shows how to identify the resulting ForceRetryError with isForceRetryError.

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

const api = ky.extend({
	retry: {limit: 1},
	hooks: {
		afterResponse: [() => ky.retry()],
		beforeRetry: [({error, retryCount}) => {
			if (isForceRetryError(error)) {
				console.log(`Forced retry #${retryCount}: ${error.code}`);
			}
		}],
	},
});

async function runForcedRetry(): Promise<void> {
	try {
		await api.get('https://api.example.com/catalog');
	} catch (error) {
		if (isForceRetryError(error)) {
			console.log('Retry limit reached:', error.code);
			return;
		}

		throw error;
	}
}

void runForcedRetry();

This differs from an ordinary retry because the response has already arrived: the beforeRetry branch can identify the explicit response-driven request, and the configured limit bounds repeated requests.

Schema-validation failures

.json() can receive a Standard Schema-compatible validator. SchemaValidationError means Ky received the response and the validator rejected its JSON value. Read its issues property. Do not classify it with isKyError(); handle it separately from transport, status, retry, and timeout failures.

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

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

async function loadUser(): Promise<void> {
	try {
		const user = await ky.get('https://api.example.com/user').json(userSchema);
		console.log('Validated user:', user.name);
	} catch (error) {
		if (error instanceof SchemaValidationError) {
			console.error('The response arrived, but validation failed:', error.issues);
			return;
		}

		if (isKyError(error)) {
			console.error('The HTTP lifecycle failed:', error.message);
			return;
		}

		throw error;
	}
}

void loadUser();

The success path receives the schema-inferred value, while the validation branch receives the validator's issues. A successful HTTP response does not guarantee a successful schema validation.

Choosing a check

For HTTP, network, and broad lifecycle checks, use the guidance in Requests and responses. This page adds the timeout, forced-retry, response-size, and schema-validation distinctions described above.

For shared defaults, replaceOption can replace a merged option when you build a derived instance; it does not change the error classification. The default export ky provides the request shortcuts and the retry control used above.

Was this page helpful?

Ky's error model — ky · GPT-5.6 Luna