sindresorhus/ky#842

Wrap fetch network failures in a new NetworkError and stop retrying unknown errors

15 files changed +397 −47

Summary

Ky now throws a new NetworkError (a KyError subclass) when fetch fails with a recognized network failure. Previously callers got the raw TypeError. The original error is on error.cause. The PR exports NetworkError and an isNetworkError type guard. It also changes the default retry logic: only TimeoutError, HTTPError, and NetworkError are retried. Any other error is thrown immediately unless shouldRetry returns true.

Detection uses isRawNetworkError, inlined from is-network-error v1.3.1. It matches a TypeError by runtime-specific message strings for Chrome, Firefox, Safari, Undici, Deno, Bun, and Cloudflare. A try/catch in the fetch path of Ky.ts performs the wrapping. Both the error type change and the narrowed retry policy break existing caller expectations. The PR also corrects stale JSDoc in options.ts and retry.ts about maxRetryAfter and timeout retries.

Changes

Flow

A fetch rejection passes through isRawNetworkError. Ky wraps recognized failures in NetworkError. The retry logic then retries only timeout, HTTP, or network errors.
Components and connections

Components

  • #options.fetch / timeout() external service
  • Ky fetch try/catch function, changed source/core/Ky.ts
  • isRawNetworkError function, changed source/utils/is-network-error.ts
  • NetworkError module, changed source/errors/NetworkError.ts
  • Ky retry delay calculation function, changed source/core/Ky.ts
  • isNetworkError function, changed source/utils/type-guards.ts
  • Caller / beforeError hooks entry point

Connections

  • #options.fetch / timeout() → Ky fetch try/catch (rejected error)
  • Ky fetch try/catch → isRawNetworkError (check error)
  • Ky fetch try/catch → NetworkError (wrap with cause)
  • NetworkError → Ky retry delay calculation (thrown error)
  • Ky retry delay calculation → isNetworkError (is retriable?)
  • Ky retry delay calculation → Caller / beforeError hooks (throw non-retriable)

Risks

1 high · 1 medium · 1 low

  1. High

    Network failures no longer surface as TypeError

    source/core/Ky.ts:832–835

    Before this PR, a fetch network failure reached callers as the raw TypeError, for example with message Failed to fetch. Now callers receive a NetworkError, which is not a TypeError. Code that checks error instanceof TypeError, error.message, or error.cause.code breaks silently. The PR's own test/headers.ts edit shows this: the undici code moves from error.cause to error.cause.cause. This is a breaking change for callers.

  2. Medium

    Unrecognized errors are no longer retried by default

    source/core/Ky.ts:490–493

    The old code fell through to #calculateDelay() for any error. The new code throws anything that is not isNetworkError. Some failures never become a NetworkError. Examples are a network error from a runtime or fetch polyfill whose message is not in isRawNetworkError's list, or a custom fetch that throws its own error type. Those failures now fail on the first attempt instead of retrying. The test/hooks.ts changes from 'network down' to 'Failed to fetch' show this regression.

  3. Low

    Name-based guard matches foreign NetworkError errors

    source/utils/type-guards.ts:75–77

    isNetworkError returns true for any object with name === 'NetworkError'. Examples are a DOM DOMException named NetworkError or another library's error class. The retry logic would then retry such errors. isKyError would also misclassify them as Ky errors, even though they lack request.

Test checklist

0 of 7 done