Errors, timeouts, and cancellation
Fetcher's default pipeline rejects HTTP statuses outside 200–299. Native fetch alone would resolve those responses, so inspect the exchange when handling a failed Fetcher request.
Choose validateStatus for a reusable HTTP acceptance policy, a request attribute only for an intentional one-call bypass, and an AbortSignal for caller-owned cancellation. None of these schedules retries. A JSON parse failure after a successful response needs a caller catch even when error interceptors are installed.
Error types and status policy
| API | Contract |
|---|---|
FetcherError(message?, cause?) | Message falls back to an Error cause's message, then a generic message; stores cause. Keeps its own stack (where the failure surfaced); the original failure and its stack stay on cause. Subclasses pass instanceof without Object.setPrototypeOf. |
ExchangeError(exchange, message?) | Stores the exchange, with cause taken from exchange.error; message falls back to its error, response statusText, then request URL. |
HttpStatusValidationError(exchange) | Created by status validation; includes status and URL. The exchange rejects with it as is (not wrapped), and .exchange.error is the same error. |
FetchTimeoutError(request) | Stores the timed-out request and a message including timeout, method (GET fallback), and URL. |
ValidateStatus | (status: number) => boolean; constructor option or new ValidateStatusInterceptor(predicate). |
IGNORE_VALIDATE_STATUS | Attribute key '__ignoreValidateStatus__'; only literal true bypasses validation. |
Validation skips exchanges with no response. An outer pipeline failure is normally ExchangeError: a status failure is the HttpStatusValidationError itself, any other error (timeout, network, abort reason, a request/response/error interceptor's throw) is wrapped with the original as cause. Only a later extractor failure can escape without that wrapper. Do not assume every failure has a response or every rejection is an Error object.
Timeout precedence
TimeoutCapable.timeout?: number uses milliseconds. resolveTimeout(requestTimeout?, optionsTimeout?) returns the request value whenever it is not undefined, including zero; otherwise the client value. timeoutFetch(request, fetchImplementation?): Promise<Response> sends with fetchImplementation, a FetchImplementation ((input: string, init?: RequestInit) => Promise<Response>; the global fetch, read at call time, by default). It passes the URL as the first argument and a clean RequestInit: url, timeout, urlParams, and abortController are left out, and cancellation arrives as the merged signal. It behaves as follows:
| Inputs | Behavior |
|---|---|
Timeout not positive (0, negative, undefined) | No timer; native fetch is aborted by request.signal or abortController.signal, whichever fires first. |
| Positive timeout | Race native fetch with a timer combined with the caller's signal and abortController; whichever fires first aborts. The timer rejects with FetchTimeoutError, a caller abort with its abort reason. |
Supply a finite positive duration to enable the timer. The timer stops when fetch resolves its Response, not when response-body consumption finishes. Streaming idle/total deadlines require caller-managed cancellation.
Ownership and cleanup
Timers are cleared on success and failure. timeoutFetch never writes to the request object and the timer never aborts a supplied controller, so the same request can be sent again. A supplied controller remains owned by the caller, including an already-aborted one. It cannot be reset for a retry. Pass a fresh controller for a new operation.
For external cancellation, pass signal or abortController; the abort reason is preserved by the failing exchange. Both apply together with Fetcher's timeout: whichever fires first aborts the request.
Complete example
import { Fetcher, ExchangeError, FetchTimeoutError } from '@ahoo-wang/fetcher';
const client = new Fetcher({
baseURL: 'https://api.example.com',
timeout: 3000,
});
const controller = new AbortController();
try {
const response = await client.get('/users/1', {
abortController: controller,
});
console.log(await response.json());
} catch (error) {
if (error instanceof ExchangeError) {
if (error.cause instanceof FetchTimeoutError) console.error('Timed out');
else console.error(error.exchange.response?.status, error.cause);
} else {
console.error(error);
}
}Public symbols and source
| Symbol | Implementation |
|---|---|
FetcherError | fetcherError.ts:40 |
ExchangeError | fetcherError.ts:83 |
FetchTimeoutError | timeout.ts:35 |
TimeoutCapable | timeout.ts:60 |
resolveTimeout | timeout.ts:81 |
FetchImplementation | timeout.ts:132 |
timeoutFetch | timeout.ts:184 |
HttpStatusValidationError | validateStatusInterceptor.ts:27 |
ValidateStatus | validateStatusInterceptor.ts:61 |
VALIDATE_STATUS_INTERCEPTOR_NAME | validateStatusInterceptor.ts:69 |
VALIDATE_STATUS_INTERCEPTOR_ORDER | validateStatusInterceptor.ts:76 |
IGNORE_VALIDATE_STATUS | validateStatusInterceptor.ts:96 |
ValidateStatusInterceptor | validateStatusInterceptor.ts:125 |
