yieldless/fetch
Native fetch helpers with tuple errors, timeouts, status checks, and JSON parsing.
yieldless/fetch keeps HTTP calls on the platform fetch() API while handling the production chores that otherwise show up in every service: tuple errors, deadlines, non-2xx responses, JSON parsing, and abort forwarding.
Exports
fetchSafe(input, options): Promise<SafeResult<Response, FetchSafeError>>fetchJsonSafe<T>(input, options): Promise<SafeResult<T, FetchJsonError>>readJsonSafe<T>(response): Promise<SafeResult<T, JsonParseError>>class HttpStatusError extends Errorclass JsonParseError extends Errorclass FetchUnavailableError extends Errortype FetchSafeOptions = RequestInit & { timeoutMs?, isOkStatus?, fetch? }
Example
import { fetchJsonSafe } from "yieldless/fetch";
const [error, user] = await fetchJsonSafe<{ id: string; name: string }>(
`https://api.example.com/users/${userId}`,
{
headers: {
accept: "application/json",
},
timeoutMs: 5_000,
signal,
},
);
if (error) {
return [error, null] as const;
}
return [null, user] as const;Fetching one record by ID
Build the URL in normal TypeScript, then pass the active AbortSignal into fetchJsonSafe().
import { fetchJsonSafe } from "yieldless/fetch";
async function fetchUserById(
apiBaseUrl: string,
userId: number,
signal: AbortSignal,
) {
const url = new URL(`/users/${String(userId)}`, apiBaseUrl);
return await fetchJsonSafe<User>(url, {
headers: { accept: "application/json" },
timeoutMs: 5_000,
signal,
});
}This shape works well inside forEach(), mapAsyncLimit(), safeRetry(), and cache loaders because all of them pass a signal to the work they run.
Status handling
fetchSafe() treats response.ok as success by default. Override isOkStatus when an API uses a status like 304 or 409 as part of a normal workflow.
const [error, response] = await fetchSafe(url, {
isOkStatus: (response) => response.ok || response.status === 304,
});Behavior notes
- Request options are ordinary
RequestInitoptions. timeoutMsderives anAbortSignaland cleans up the timer when the request settles.- Non-ok responses return
HttpStatusErrorwith the originalresponseattached. fetchJsonSafe()only parses JSON afterfetchSafe()succeeds.readJsonSafe()wraps parser failures inJsonParseErrorinstead of throwing.
Good
Keep HTTP concerns at the HTTP boundary.
const [error, user] = await fetchJsonSafe<User>(url, {
headers: { accept: "application/json" },
timeoutMs: 5_000,
signal,
});
if (error instanceof HttpStatusError && error.status === 404) {
return [new NotFoundError("User not found"), null] as const;
}Inject a custom fetch in tests or runtimes that do not expose global fetch.
await fetchJsonSafe(url, {
fetch: testFetch,
});Avoid
Do not parse JSON before checking status.
const response = await fetch(url);
const body = await response.json();
if (!response.ok) {
return [new Error(body.message), null] as const;
}Use fetchSafe() when you need the raw response and fetchJsonSafe() when success means "valid JSON body".