Skip to content
D
Documentation

Retries, codes, and token refresh

how-to
2 min readUpdated

Configure retries, retry codes, and token refresh

Use ky when you need to decide which failures are retried, identify a forced retry with a machine-readable code, or replace an expired credential before the next attempt.

Choose when Ky retries

Set retry.limit and retry.statusCodes for the ordinary retry policy. Use HTTPError when the policy depends on an HTTP response, and use shouldRetry when it also depends on the retry count. It runs only after the method and retry-limit checks, and its result controls the decision: true forces a retry, false prevents one, and undefined keeps Ky's default checks.

beforeRetry has a different job. Ky calls it after a retry has been selected, so use it to change the request rather than to decide whether the retry happens.

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

async function main(): Promise<void> {
	const response = await ky.get('https://api.example.com/report', {
	retry: {
		limit: 3,
		methods: ['get'],
		statusCodes: [429, 500, 503],
		shouldRetry: ({error, retryCount}) => {
			if (error instanceof HTTPError && error.response.status === 429) {
				return retryCount <= 2;
			}

			if (error instanceof HTTPError && error.response.status >= 400 && error.response.status < 500) {
				return false;
			}

			return undefined;
		},
	},
	});

console.log(response.status);
}

void main();

The request retries up to three times for the configured GET statuses. A 429 retries only on the first two retry decisions; other client errors do not retry. Returning undefined leaves network errors and the configured status-code behavior to Ky's default logic.

The default retry delay grows exponentially. Set retry.delay to calculate it from the attempt count, retry.backoffLimit to cap each delay, and retry.jitter to add randomness. A server retry-timing header takes precedence over jitter. Set retry.retryOnTimeout: true when a timeout before receiving a response is also retryable. A timeout while a shortcut method reads an already received body is not retried.

Retries buffer a streaming request body through tee(). That can consume substantial memory, so set retry: {limit: 0} for a large streaming upload that does not need retries.

Force a retry and attach a code

The addition here is a response-content-triggered retry—even after a successful HTTP status—with a reason carried into the retry hook. For ordinary retry selection and hook roles, see How Ky works.

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

const api = ky.extend({
	retry: {limit: 2},
	hooks: {
		afterResponse: [
			async ({response, retryCount}) => {
				if (response.status !== 200) {
					return response;
				}

				const data = await response.clone().json<{ready: boolean}>();
				if (!data.ready && retryCount === 0) {
					return ky.retry({code: 'REPORT_NOT_READY'});
				}

				return response;
			},
		],
		beforeRetry: [
			({error, retryCount}) => {
				if (isForceRetryError(error)) {
					console.log(`Forced retry #${retryCount}: ${error.code}`);
				}
			},
		],
	},
});

async function main(): Promise<void> {
	const report = await api.get('https://api.example.com/report').json<{ready: boolean}>();
	console.log(report.ready);
}

void main();

When the response says ready: false on the first attempt, Ky selects another attempt and the beforeRetry hook sees the REPORT_NOT_READY code. The sample logs the later response's ready field; it is undefined if the service omits that field. If the retry limit is exhausted, the forced retry is surfaced as a ForceRetryError through Ky's error lifecycle.

Distinguish the failure you handle

The addition here is one caller branch that reports the relevant request detail for each failure. For the guards' meanings, see How Ky works.

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

async function main(): Promise<void> {
	try {
		await ky.get('https://api.example.com/account', {timeout: 5_000}).json();
	} catch (error) {
		if (isHTTPError(error)) {
			console.error('HTTP status:', 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 {
			throw error;
		}
	}
}

void main();

See error handling and validation for the response-error body handling and error model.

Was this page helpful?