Cancel stale requests

Use one controller per search and a latest-request check. Cancellation saves work when the transport supports it; the identity check also prevents stale results from updating the UI.

import { Http, rest } from "fetchzen";
interface SearchItem {
id: string;
title: string;
}
export function createSearch(
render: (items: SearchItem[]) => void,
reportError: (error: unknown) => void,
) {
const api = Http.create("https://api.example.com", {
preset: rest,
cache: false,
});
let active: AbortController | undefined;
async function search(query: string): Promise<void> {
active?.abort();
const controller = new AbortController();
active = controller;
try {
const { data } = await api.get<SearchItem[], { q: string }>("/search", {
params: { q: query },
signal: controller.signal,
timeout: 5_000,
});
if (active === controller && !controller.signal.aborted) {
render(data);
}
} catch (error) {
if (active !== controller || controller.signal.aborted) return;
reportError(error);
} finally {
if (active === controller) active = undefined;
}
}
function dispose(): void {
active?.abort();
active = undefined;
}
return { search, dispose };
}

Call search(query) when the input changes and dispose() when the view is removed. An aborted signal cannot be reused, so each search gets a fresh controller.

signal and a positive timeout are composed: either can abort the fetch. No timeout is set by default; timeout: 0 disables an inherited timeout. A normal abort() produces an AbortError, while the library timeout uses TimeoutError. This example suppresses superseded/disposed requests but reports timeouts and other current-request failures.

An injected fetch must honor init.signal itself. Aborting does not guarantee that the server stopped processing the request.

Related: test with injected fetch. Try timeout and cancellation.