Fetching & Promises
Syntax
Promise Constructor
const p = new Promise<string>((resolve, reject) => {
const ok = true;
if (ok) resolve("done"); // fulfills with value
else reject("fail"); // rejects with reason
});Then / Catch / Finally
fetch("/api/data")
.then((res) => res.json()) // handle fulfilled
.catch((err) => console.error(err)) // handle rejected
.finally(() => cleanup()); // always runsAsync / Await
async function getData(): Promise<string> {
const res = await fetch("/api/data");
const data: string = await res.json();
return data;
}Fetch Options
const controller = new AbortController();
fetch("/api/items", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "Widget" }),
signal: controller.signal,
});Response Methods
const res = await fetch("/api");
await res.json(); // parse JSON body
await res.text(); // plain text
await res.blob(); // binary data
res.ok; // true if 200-299
res.status; // 404, 500, etc.Promise Static Methods
Promise.all([p1, p2]); // all must fulfill
Promise.allSettled([p1, p2]); // wait for all, never rejects
Promise.race([p1, p2]); // first to settle wins
Promise.any([p1, p2]); // first to fulfill winsBeginner Patterns
GET Request
async function getUser(id: number) {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error(`Status: ${res.status}`);
const user: User = await res.json();
return user;
}POST Request
async function createUser(name: string) {
const res = await fetch("/api/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name }),
});
return res.json() as Promise<User>;
}Try / Catch
async function loadData(url: string) {
try {
const res = await fetch(url);
return await res.json();
} catch (err) {
console.error("Fetch failed:", err);
return null;
}
}Typing a Response
interface User {
id: number;
name: string;
email: string;
}
const res = await fetch("/api/user");
const user = (await res.json()) as User;
// user.name is now typedQuery Params
async function search(term: string, page: number) {
const params = new URLSearchParams({
q: term,
page: String(page),
});
const res = await fetch(`/api/search?${params}`);
return res.json();
}Chaining Promises
fetch("/api/user/1")
.then((res) => res.json())
.then((user: User) => fetch(`/api/posts?author=${user.id}`))
.then((res) => res.json())
.then((posts: Post[]) => console.log(posts));Intermediate Patterns
Parallel Requests
async function getDashboard(userId: number) {
const [user, posts, stats] = await Promise.all([
fetch(`/api/users/${userId}`).then((r) => r.json()),
fetch(`/api/posts?author=${userId}`).then((r) => r.json()),
fetch(`/api/stats/${userId}`).then((r) => r.json()),
]);
return { user, posts, stats } as Dashboard;
}Abort Controller
const controller = new AbortController();
fetch("/api/slow", { signal: controller.signal })
.then((res) => res.json())
.catch((err) => {
if (err.name === "AbortError") return; // expected
throw err;
});
controller.abort(); // cancels the requestTimeout
async function fetchWithTimeout(url: string, ms: number) {
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), ms);
try {
const res = await fetch(url, { signal: controller.signal });
return await res.json();
} finally {
clearTimeout(id);
}
}Retry Logic
async function fetchRetry(url: string, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
const res = await fetch(url);
if (res.ok) return res.json();
} catch {
if (i === retries - 1) throw new Error("Max retries");
}
await new Promise((r) => setTimeout(r, 1000 * 2 ** i));
}
throw new Error("Max retries");
}AllSettled
const results = await Promise.allSettled([
fetch("/api/a").then((r) => r.json()),
fetch("/api/b").then((r) => r.json()),
]);
results.forEach((r) => {
if (r.status === "fulfilled") console.log(r.value);
if (r.status === "rejected") console.error(r.reason);
});Generic Fetcher
async function fetcher<T>(url: string): Promise<T> {
const res = await fetch(url);
if (!res.ok) throw new Error(res.statusText);
return res.json() as Promise<T>;
}
const user = await fetcher<User>("/api/user/1");Advanced Patterns
Request Interceptor
async function authFetch(url: string, opts?: RequestInit) {
let token = getToken();
let res = await fetch(url, {
...opts,
headers: { ...opts?.headers, Authorization: `Bearer ${token}` },
});
if (res.status === 401) {
await refreshToken();
token = getToken();
res = await fetch(url, {
...opts,
headers: { ...opts?.headers, Authorization: `Bearer ${token}` },
});
}
return res;
}Race with Timeout
async function raceTimeout<T>(promise: Promise<T>, ms: number) {
const timeout = new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error("Timeout")), ms)
);
return Promise.race([promise, timeout]);
}
const data = await raceTimeout(fetch("/api"), 5000);Streaming Response
const res = await fetch("/api/stream");
const reader = res.body!.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
console.log(decoder.decode(value)); // chunk
}Typed Error Handling
class FetchError extends Error {
constructor(public status: number, message: string) {
super(message);
}
}
async function safeFetch<T>(url: string): Promise<T> {
const res = await fetch(url);
if (!res.ok) throw new FetchError(res.status, res.statusText);
return res.json() as Promise<T>;
}Sequential Pipeline
async function pipeline(ids: number[]) {
const results: User[] = [];
for (const id of ids) {
const user = await fetcher<User>(`/api/users/${id}`);
results.push(user); // sequential, respects rate limits
}
return results;
}Batch with Concurrency
async function batch<T>(urls: string[], limit = 3) {
const results: T[] = [];
for (let i = 0; i < urls.length; i += limit) {
const chunk = urls.slice(i, i + limit);
const data = await Promise.all(
chunk.map((url) => fetcher<T>(url))
);
results.push(...data);
}
return results;
}Deduped Fetch
const cache = new Map<string, Promise<unknown>>();
function dedupeFetch<T>(url: string): Promise<T> {
if (!cache.has(url)) {
cache.set(url, fetcher<T>(url).finally(() => cache.delete(url)));
}
return cache.get(url) as Promise<T>;
}
// concurrent calls to same URL share one requestLast updated on