Wrap fetch network failures in a new NetworkError and stop retrying unknown errors
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
-
Modified source/core/Ky.ts +35 −14 The fetch call, with or without
timeout, now sits in atry/catch. The catch rethrowsNetworkError(this.request, {cause})whenisRawNetworkErrormatches. The retry-delay logic now returns#calculateDelay()explicitly for timeouts and HTTP errors. It throws any error that is notisNetworkError.- What
- The fetch call, with or without
timeout, now sits in atry/catch. The catch rethrowsNetworkError(this.request, {cause})whenisRawNetworkErrormatches. The retry-delay logic now returns#calculateDelay()explicitly for timeouts and HTTP errors. It throws any error that is notisNetworkError. - Why
- Callers and hooks now see
NetworkErrorinstead ofTypeError. Errors that were retried before, such as arbitrary thrown errors from a customfetch, are no longer retried by default.
-
Added source/utils/is-network-error.ts +49 −0 New
isRawNetworkError. It checks for aTypeErrorwhose message matches a known set of network-failure messages. It also handles special cases for SafariLoad failed, Deno, and ChromeFailed to fetch (host).- What
- New
isRawNetworkError. It checks for aTypeErrorwhose message matches a known set of network-failure messages. It also handles special cases for SafariLoad failed, Deno, and ChromeFailed to fetch (host). - Why
- This heuristic decides both wrapping and retry eligibility. An unmatched message means no wrapping and no retry.
-
Added source/errors/NetworkError.ts +17 −0 New
NetworkError extends KyError. It hasname = 'NetworkError', arequestproperty, and the messageRequest failed due to a network error: METHOD URL. It forwardscausetosuper.- What
- New
NetworkError extends KyError. It hasname = 'NetworkError', arequestproperty, and the messageRequest failed due to a network error: METHOD URL. It forwardscausetosuper. - Why
- This is the new public error type for network failures.
-
Modified source/utils/type-guards.ts +24 −1 Adds
isNetworkError, which checksinstanceof NetworkErrororname === 'NetworkError'.isKyErrornow also acceptsisNetworkError.- What
- Adds
isNetworkError, which checksinstanceof NetworkErrororname === 'NetworkError'.isKyErrornow also acceptsisNetworkError. - Why
Ky.tsusesisNetworkErrorto decide retries. Users get a cross-realm guard.
-
Modified source/index.ts +2 −0 Exports
NetworkErrorandisNetworkError.- What
- Exports
NetworkErrorandisNetworkError. - Why
- This adds to the public API surface.
-
Modified source/types/options.ts +4 −6 Rewrites the
retryJSDoc. It lists all retry fields and documents network-error retries. It also fixes themaxRetryAfterbehavior description. It drops the delay formula and the outdated sentence 'Retries are not triggered following a timeout'.- What
- Rewrites the
retryJSDoc. It lists all retry fields and documents network-error retries. It also fixes themaxRetryAfterbehavior description. It drops the delay formula and the outdated sentence 'Retries are not triggered following a timeout'. - Why
- Documentation only.
-
Modified source/types/retry.ts +2 −2 JSDoc fixes for
maxRetryAfter. TheshouldRetrydocs now state that unrecognized errors are not retried.- What
- JSDoc fixes for
maxRetryAfter. TheshouldRetrydocs now state that unrecognized errors are not retried. - Why
- Documentation only.
-
Modified source/types/hooks.ts +3 −3 JSDoc for
beforeRetryandbeforeErrornow mentionsNetworkErrorandisNetworkError.- What
- JSDoc for
beforeRetryandbeforeErrornow mentionsNetworkErrorandisNetworkError. - Why
- Documentation only.
-
Modified readme.md +29 −5 Documents network-error retries and the
NetworkErrorsection with an example. It updates hook,HTTPError, andKyErrortext.- What
- Documents network-error retries and the
NetworkErrorsection with an example. It updates hook,HTTPError, andKyErrortext. - Why
- Documentation only.
-
Modified source/errors/HTTPError.ts +1 −1 JSDoc mentions the
optionsproperty.- What
- JSDoc mentions the
optionsproperty. - Why
- Documentation only.
-
Modified source/errors/KyError.ts +1 −1 JSDoc lists
NetworkErroras a subclass.- What
- JSDoc lists
NetworkErroras a subclass. - Why
- Documentation only.
-
Modified test/retry.ts +216 −1 Adds tests for wrapping, default retry, POST non-retry, and non-network
TypeErrorpassthrough. It also coversshouldRetryforcing and fallthrough,beforeErrorcause chain, andtimeout: false.- What
- Adds tests for wrapping, default retry, POST non-retry, and non-network
TypeErrorpassthrough. It also coversshouldRetryforcing and fallthrough,beforeErrorcause chain, andtimeout: false. - Why
- These tests cover the new behavior.
-
Modified test/hooks.ts +10 −9 Simulated network failures now throw
TypeError('Failed to fetch')instead of arbitrary messages. Assertions useisNetworkError.- What
- Simulated network failures now throw
TypeError('Failed to fetch')instead of arbitrary messages. Assertions useisNetworkError. - Why
- The old arbitrary messages are no longer recognized or retried. These test edits show the behavior change for custom
fetchimplementations.
-
Modified test/headers.ts +2 −1 Reads the undici error code from
error.cause.causeinstead oferror.cause.- What
- Reads the undici error code from
error.cause.causeinstead oferror.cause. - Why
- The extra
NetworkErrorlayer moves the cause one level deeper.
-
Modified test/memory-leak.ts +2 −3 Expects
NetworkErrorinstead ofTypeErrorwithfetch failed.- What
- Expects
NetworkErrorinstead ofTypeErrorwithfetch failed. - Why
- This reflects the new error type.
Flow
Rendering diagram…
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
-
Highsource/core/Ky.ts:832–835
Network failures no longer surface as
TypeErrorBefore this PR, a fetch network failure reached callers as the raw
TypeError, for example with messageFailed to fetch. Now callers receive aNetworkError, which is not aTypeError. Code that checkserror instanceof TypeError,error.message, orerror.cause.codebreaks silently. The PR's owntest/headers.tsedit shows this: the undici code moves fromerror.causetoerror.cause.cause. This is a breaking change for callers. -
Mediumsource/core/Ky.ts:490–493
Unrecognized errors are no longer retried by default
The old code fell through to
#calculateDelay()for any error. The new code throws anything that is notisNetworkError. Some failures never become aNetworkError. Examples are a network error from a runtime or fetch polyfill whose message is not inisRawNetworkError's list, or a customfetchthat throws its own error type. Those failures now fail on the first attempt instead of retrying. Thetest/hooks.tschanges from'network down'to'Failed to fetch'show this regression. -
Lowsource/utils/type-guards.ts:75–77
Name-based guard matches foreign
NetworkErrorerrorsisNetworkErrorreturns true for any object withname === 'NetworkError'. Examples are a DOMDOMExceptionnamedNetworkErroror another library's error class. The retry logic would then retry such errors.isKyErrorwould also misclassify them as Ky errors, even though they lackrequest.