Skip to content
D
Documentation

Handle request errors

how-to
3 min readUpdated

Use this approach when your application needs different handling for an HTTP error response, a failed connection, or a timeout. ky treats non-2xx responses as errors by default and retries eligible failures before rejecting the request.

The samples run in a browser or a Node.js project with Fetch support. Install Ky in your project:

bash
npm install ky

1. Distinguish the failure and read its data

An HTTPError gives you response, request, normalized options, and pre-parsed data. Use isHTTPError, isNetworkError, and isTimeoutError to narrow the caught value before accessing error-specific properties.

  • NetworkError represents a network failure, such as DNS failure, connection refusal, or being offline. Read request.url and the original error in cause; a fetch-phase network failure has no HTTP response.
  • TimeoutError identifies an exceeded timeout and gives you the affected request.
  • isKyError catches other failures in Ky's HTTP lifecycle. Keep a separate fallback for errors outside that lifecycle.

This request targets a nonexistent GitHub API endpoint. If the service returns a non-2xx response, the sample logs the HTTP status and parsed error body; if it returns a successful JSON response, the sample logs the response data instead. retry: 0 disables retries so the example handles one attempt.

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

void (async () => {
	try {
		const data = await ky.get('https://api.github.com/ky-error-example-not-found', {
			retry: 0,
			timeout: 5000,
		}).json();
		console.log('Response data:', data);
	} catch (error) {
		if (isHTTPError(error)) {
			console.error('HTTP status:', error.response.status);
			console.error('Content type:', error.response.headers.get('content-type'));
			console.error('Error body:', error.data);
		} else if (isNetworkError(error)) {
			console.error('Network failure:', error.request.url, error.cause);
		} else if (isTimeoutError(error)) {
			console.error('Timeout:', error.request.url);
		} else if (isKyError(error)) {
			console.error('Ky failure:', error.message);
		} else {
			console.error('Other failure:', error);
		}
	}
})();

Ky parses JSON error bodies based on Content-Type, using parseJson when supplied or JSON.parse otherwise. Other content types produce text. data can be undefined when the body is empty, unreadable, too large, fails parsing, or exceeds the error-data read/parse timeout. Check its shape before reading fields.

Use the caught error's pre-parsed body for recovery decisions; see Request lifecycle for body consumption and response metadata.

Response-size failures require the next release

Ky 2.1.0 does not include maxResponseSize, ResponseSizeError, or isResponseSizeError. These APIs require the next release; do not import them in a 2.1.0 project.

With that release, maxResponseSize limits consumed response-stream bytes after decompression, independently of Content-Length. Exceeding it throws ResponseSizeError without automatic retries. Use isResponseSizeError() to distinguish it, then read error.maxResponseSize for the configured byte limit and error.request for the request. This is a body-size limit, not an HTTP status failure or a total-memory limit.

2. Treat expected HTTP errors as normal responses

Set throwHttpErrors: false when checking resource availability and expecting an error response. The call returns the response, leaving you to inspect ok or status and consume its body.

ts
import ky from 'ky';

void (async () => {
	try {
		const response = await ky.get('https://api.github.com/ky-error-example-not-found', {
			throwHttpErrors: false,
		});

		if (response.status === 404) {
			console.log('The resource is unavailable.');
		} else if (!response.ok) {
			console.error('Unexpected HTTP status:', response.status);
		} else {
			console.log('Response body:', await response.text());
		}
	} catch (error) {
		console.error('The request failed without a normal HTTP response:', error);
	}
})();

Disabling HTTP errors also disables status-based automatic retries. Ky considers those error responses successful. Keep throwHttpErrors enabled when relying on status-based retries; disable it only when error responses belong to normal application flow. Network failures and timeouts can still reject the call.

For selective handling, pass a function such as status => status !== 404. Ky then returns 404 responses normally but throws for other non-2xx statuses. Prefer the boolean form unless you need that distinction.

3. Customize the error before it reaches your catch block

Use hooks.beforeError to modify an error right before Ky throws it. The hook receives request, normalized options, error, and retryCount, and returns an Error or a promise of one. retryCount is 0 for the initial attempt and increments with each retry.

The example follows the error body's message field, when it is a string, and adds the HTTP status. Returning the same error preserves its HTTP-specific properties for the catch block. If the body lacks that field, the original message remains unchanged.

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

const api = ky.extend({
	hooks: {
		beforeError: [
			({error}) => {
				if (
					isHTTPError(error)
					&& typeof error.data === 'object'
					&& error.data !== null
					&& 'message' in error.data
					&& typeof error.data.message === 'string'
				) {
					error.message = `${error.data.message} (${error.response.status})`;
				}

				return error;
			},
		],
	},
});

void (async () => {
	try {
		await api.get('https://api.github.com/ky-error-example-not-found').json();
	} catch (error) {
		if (isHTTPError(error)) {
			console.error(error.message);
		} else {
			console.error(error);
		}
	}
})();

error.data is populated before beforeError runs. Use the shortcut form await ky(url).json() to send body-read network and timeout failures through this hook as well. Errors from reading an already returned response are outside the request lifecycle.

Options that matter

OptionTypeDefaultWhat it does
throwHttpErrorsboolean | ((status: number) => boolean)trueControls whether non-2xx responses throw an HTTP error.
hooks.beforeErrorArray of functions returning Error | Promise<Error>[]Modifies or replaces the error before it is thrown.
timeoutnumber | false10000Sets the per-attempt timeout in milliseconds; shortcuts also use it as a separate body-read timeout.
totalTimeoutnumber | falsefalseBounds the entire operation, including retries and delays. It does not bound beforeError hooks.

Handle schema rejection separately

When using .json(schema), handle SchemaValidationError explicitly with error instanceof SchemaValidationError and read its issues. It does not extend KyError and isKyError() does not match it: the request succeeded, but your schema rejected the data. See Send and validate JSON for the validation sample.

Was this page helpful?