Module 59 : Async/Await and APIs in Javascript.
1 Core concepts
Promises (recap)
A Promise represents a value that may be available now, later, or never.
Key states: pending → fulfilled (resolved) or rejected. Use .then() for success, .catch() for failure. async/await is syntactic sugar over Promises that makes asynchronous code look synchronous.
Event loop & microtasks
await pauses the async function, but it doesn’t block the thread. The rest of the code runs — the resolved value is scheduled as a microtask (runs right after current synchronous code completes). Understanding microtasks helps explain execution order of Promises vs setTimeout.
async / await syntax
async function getUser(id) { const res = await fetch(`/api/users/${id}`); if (!res.ok) throw new Error(`HTTP ${res.status}`); return res.json(); }
An async function always returns a Promise.
Use await to get the resolved value of a Promise. Surround with try/catch for error handling.
Error handling
try { const data = await getUser(1); } catch (err) { // handle network errors, HTTP errors, JSON parse errors... }
If fetch is aborted or the network fails it rejects (throws). If res.ok is false you may want to throw manually.
Sequential vs Parallel
Sequential:
const a = await fetchA(); const b = await fetchB(); // waits for A first
Parallel:
const [a, b] = await Promise.all([fetchA(), fetchB()]);
Promise.all fails fast (rejects on first rejection). Use allSettled when you want results even if some fail.
Cancellation: AbortController
Browser fetch supports AbortController. Abort signals cause fetch to reject with an AbortError.
const ac = new AbortController(); fetch(url, { signal: ac.signal }); // cancel later: ac.abort();
Fetch API — quick reference
fetch(input, init) returns a Response. Important Response methods:
res.json()
res.text()
res.blob()
res.arrayBuffer()
res.body (for streaming: ReadableStream)
Fetch init common options:
method, headers, body, credentials, mode, cache, signal (AbortController).
Handling different response types
JSON: await res.json() (catches parse errors)
Text: await res.text()
Binary: await res.blob() or arrayBuffer()
Streaming responses (NDJSON, chunked)
You can process response increments using res.body.getReader() and TextDecoder. Useful for streaming APIs (progressive updates).
Authentication & headers
API key / Bearer token: Authorization: Bearer <token>
For browser apps: prefer secure cookies (HttpOnly, SameSite) for session tokens; avoid storing secrets in front-end code.
CORS basics
Cross-origin requests require the server to send appropriate Access-Control-* headers. Preflight OPTIONS requests are used when request uses non-simple headers or methods.
Retry & backoff
Use exponential backoff with jitter for retrying idempotent requests. Avoid retrying non-idempotent requests (POST without idempotency guarantees).
Uploading files & progress
Use FormData for multipart uploads. For progress you usually rely on XMLHttpRequest or Axios (which has onUploadProgress), because fetch lacks upload progress in some environments.
2 (well-tested patterns)
Below are small utilities you can drop into projects.
1) fetchJSON with status check
async function fetchJSON(url, options = {}) { const res = await fetch(url, options); if (!res.ok) { const text = await res.text().catch(() => ''); throw new Error(`HTTP ${res.status} ${res.statusText} ${text ? ' - ' + text : ''}`); } return res.json(); }
2) fetchWithTimeout
async function fetchWithTimeout(url, options = {}, timeout = 8000) { const controller = new AbortController(); const id = setTimeout(() => controller.abort(), timeout); try { const res = await fetch(url, { ...options, signal: controller.signal }); if (!res.ok) throw new Error(`HTTP ${res.status}`); return res; } finally { clearTimeout(id); } }
3) retryWithBackoff
function wait(ms) { return new Promise(r => setTimeout(r, ms)); } async function retryWithBackoff(fn, { retries = 3, minDelay = 300, factor = 2, jitter = 100 } = {}) { let attempt = 0; while (true) { try { return await fn(); } catch (err) { attempt++; if (attempt > retries) throw err; const backoff = Math.pow(factor, attempt - 1) * minDelay; const sleep = backoff + Math.floor(Math.random() * jitter); await wait(sleep); } } }
4) Debounce helper (useful for search inputs)
function debounce(fn, wait = 300) { let id; return (...args) => { clearTimeout(id); id = setTimeout(() => fn(...args), wait); }; }
5) Abortable search (debounce + AbortController)
let currentController = null; const search = debounce(async (query) => { if (currentController) currentController.abort(); currentController = new AbortController(); try { const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`, { signal: currentController.signal }); const data = await res.json(); renderResults(data); } catch (err) { if (err.name === 'AbortError') return; // expected console.error(err); } finally { currentController = null; } }, 300); input.addEventListener('input', e => search(e.target.value));
6) Streaming NDJSON parser
async function streamNDJSON(url, onItem) { const res = await fetch(url); if (!res.body) throw new Error('ReadableStream not supported'); const reader = res.body.getReader(); const decoder = new TextDecoder(); let buf = ''; while (true) { const { done, value } = await reader.read(); if (done) break; buf += decoder.decode(value, { stream: true }); const lines = buf.split('\n'); buf = lines.pop(); for (const line of lines) { if (line.trim()) onItem(JSON.parse(line)); } } if (buf.trim()) onItem(JSON.parse(buf)); }
3 Concrete
Example A — Basic JSON fetch (browser)
<!-- index.html --> <input id="userId" value="1" /> <button id="btn">Load</button> <pre id="out"></pre> <script> async function loadUser(id) { const res = await fetch(`/api/users/${id}`); if (!res.ok) throw new Error('Failed to load user'); return res.json(); } document.getElementById('btn').addEventListener('click', async () => { const id = document.getElementById('userId').value; try { const user = await loadUser(id); document.getElementById('out').textContent = JSON.stringify(user, null, 2); } catch (err) { document.getElementById('out').textContent = 'Error: ' + err.message; } }); </script>
Explanation: loadUser awaits the fetch. If fetch resolves with ok=false (e.g., 404), we throw to let caller handle errors.
Example B — Parallel fetch of multiple endpoints
async function loadDashboard(userId) { const userPromise = fetchJSON(`/api/users/${userId}`); const feedPromise = fetchJSON(`/api/users/${userId}/feed`); const notificationsPromise = fetchJSON(`/api/users/${userId}/notifications`); // Run in parallel, wait for all: const [user, feed, notifications] = await Promise.all([userPromise, feedPromise, notificationsPromise]); return { user, feed, notifications }; }
Explanation: starting promises immediately and await-ing Promise.all makes the requests concurrent.
Example C — Retry with backoff for GET requests
async function getWithRetry(url) { return retryWithBackoff(() => fetchJSON(url), { retries: 4 }); }
Explanation: retries network blips. Only use for safe idempotent requests.
Example D — Upload file with progress (Axios)
import axios from 'axios'; const formData = new FormData(); formData.append('file', fileInput.files[0]); await axios.post('/upload', formData, { headers: { 'Content-Type': 'multipart/form-data' }, onUploadProgress: (progressEvent) => { const percent = Math.round((progressEvent.loaded * 100) / progressEvent.total); console.log('Upload', percent, '%'); } });
Explanation: fetch currently lacks consistent upload progress events across browsers, so Axios or XMLHttpRequest is used for progress UI.
4 Exercises (explanations)
Exercise 1 — Simple fetch
Fetch posts from JSONPlaceholder (https://jsonplaceholder.typicode.com/posts) and display first 10 titles.
Hint/solution: use await fetch → res.json() → slice and render.
Exercise 2 — Debounced search with cancel
Make a search input that hits https://jsonplaceholder.typicode.com/users?name_like=<q> with 300ms debounce. Cancel previous fetch on new input with AbortController.
Explanation: debounce reduces requests; AbortController prevents race conditions and wasted work.
Exercise 3 — Parallel vs Sequential timing
Write two functions, sequentialFetch(urls) and parallelFetch(urls). Measure time (performance.now()) when fetching 5 endpoints. Observe parallel is faster when network concurrency is available.
Explanation: illustrate cost of awaiting each request vs kicking off all promises then awaiting them together.
Exercise 4 — Retry + Backoff
Write getWithRetry(url) that retries up to 4 times with exponential backoff and jitter. Test this by creating a mock API that fails the first 2 tries.
Explanation: show retryWithBackoff utility in action.
Exercise 5 (Advanced) — Infinite scroll + cache + cancelable search
Build a single-page app that:
Shows posts from JSONPlaceholder with infinite scrolling (fetch next page when user nears bottom).
Implements a search bar with debounce + AbortController cancel.
Caches fetched pages in localStorage or IndexedDB to avoid re-fetching.
Handles errors gracefully and implements retry for GETs.
Solution outline: use page parameter, track isLoading, fetch pages in parallel when necessary, store results with timestamp in cache and validate TTL.
5 Full “Searchable Infinite Posts”
Purpose: Put everything together — fetch, concurrency, abort, caching, infinite scroll.
Stack: HTML + Vanilla JS (no frameworks) + public API https://jsonplaceholder.typicode.com/posts.
File structure
lab/
index.html
main.js
styles.css
Step 1 — Basic UI
A search input and a container for posts + spinner.
Step 2 — Fetch helper
Add fetchJSON and fetchWithTimeout utilities.
Step 3 — Search with debounce + abort
Debounce user input.
Abort previous search fetch when a new input arrives.
If query is empty, show paginated feed.
Step 4 — Infinite scroll
Track currentPage.
On scroll near bottom, call loadPage(page+1).
isLoading prevents duplicate loads.
Step 5 — Caching
Store fetched pages in localStorage with a TTL (e.g., 5 minutes).
On load, check cache first.
Step 6 — Retry for transient errors
Wrap GETs with retryWithBackoff.
Step 7 — Polishing
Error messages & retry button
Loading indicators
UX: disable next fetch if no more pages
Example snippet (main pieces)
// main.js (abridged) const cacheTTL = 5 * 60 * 1000; function cacheGet(key) { try { const raw = localStorage.getItem(key); if (!raw) return null; const { ts, data } = JSON.parse(raw); if (Date.now() - ts > cacheTTL) { localStorage.removeItem(key); return null; } return data; } catch { return null; } } function cacheSet(key, data) { localStorage.setItem(key, JSON.stringify({ ts: Date.now(), data })); } // fetch page async function loadPage(page = 1) { const cacheKey = `posts-page-${page}`; const cached = cacheGet(cacheKey); if (cached) return cached; const url = `https://jsonplaceholder.typicode.com/posts?_page=${page}&_limit=10`; const data = await retryWithBackoff(() => fetchJSON(url)); cacheSet(cacheKey, data); return data; }
Assessment
Working infinite scroll: 30%
Debounce + cancel: 20%
Caching + TTL: 20%
Error handling & retry: 15%
Code quality + comments: 15%
6 Testing & mocking HTTP
Unit test approach
Use Jest for unit tests.
Use msw (Mock Service Worker) to intercept and mock fetch() on both browser tests and node tests (msw supports node). This avoids calling network endpoints and gives realistic behavior.
Simple test example (Jest + msw)
// __tests__/fetch.test.js import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { fetchJSON } from '../main'; const server = setupServer( rest.get('https://jsonplaceholder.typicode.com/posts', (req, res, ctx) => { return res(ctx.json([{ id: 1, title: 'hello' }])); }) ); beforeAll(() => server.listen()); afterEach(() => server.resetHandlers()); afterAll(() => server.close()); test('fetchJSON returns posts', async () => { const data = await fetchJSON('https://jsonplaceholder.typicode.com/posts'); expect(Array.isArray(data)).toBe(true); expect(data[0].title).toBe('hello'); });
Benefits: test both success and failure scenarios (network error, timeouts, 500 responses).
7 Security
Never embed secret API keys in client-side code. Use a proxy server or signed short-lived tokens.
Prefer HttpOnly cookies for authentication where appropriate (prevents XSS reading).
Sanitize data and avoid using innerHTML with untrusted content.
Use Content-Security-Policy (CSP), X-Frame-Options, and other headers.
Validate server responses — do not assume schema correctness.
Performance
Use caching: Cache-Control, ETag/If-None-Match, and client-side caches (IndexedDB) for offline.
Use Promise.all for independent requests.
Use HTTP/2 or HTTP/3 where possible for multiplexing.
Use pagination to limit response sizes.
Reliability
Use retries with exponential backoff and jitter for transient network errors.
Respect rate limits returned by APIs; implement client-side rate limiting.
Add health checks on servers and circuit breakers in client-side heavy systems.
8 Advanced topics
Streaming & incremental UI updates: use res.body.getReader() to process large responses or server-sent chunks.
Server-Sent Events (SSE) for one-way streaming: good for feed-like updates.
WebSockets for low-latency bi-directional communication.
GraphQL: how async patterns differ for queries and subscriptions.
Service Workers for offline caching of API results and background sync.
OAuth2 flows: Authorization Code flow (with PKCE) for SPAs — design considerations.
gRPC-web for binary, typed remote calls.
9 Research coding specialist
Good topics to take to a specialist for discussion:
Cancellation patterns — compare AbortController vs token-based cancellation across libraries and polyfills; how to cancel server-side processing safely.
Streaming APIs — example with NDJSON vs SSE vs chunked JSON: performance & parsing strategies.
Idempotency & retries — how to design backend idempotency keys for safe retries on POST.
Rate limiting & backpressure — client strategies when API returns 429, and token bucket algorithms on the client.
Testing network resilience — chaos testing for flaky networks; how to mock timeouts/latency in integration tests.
Security — token storage tradeoffs (cookies vs localStorage) in complex SPAs with third-party scripts.
Observability — best metrics for client API health (latency histograms, error rates, % of aborted requests).
Suggested reading / reference list
MDN: fetch, Promises, AbortController
WHATWG Fetch Standard
ECMAScript Language Specification (Promises and async functions)
Jake Archibald’s posts on streams & progressive loading
10 Quick sheet (common patterns)
Check status + parse JSON
const res = await fetch(url); if (!res.ok) throw new Error(res.status); const data = await res.json();
Timeout
const controller = new AbortController(); setTimeout(() => controller.abort(), 5000); await fetch(url, { signal: controller.signal });
Parallel
const [a, b] = await Promise.all([fetchA(), fetchB()]);
Graceful parallel (some can fail)
const results = await Promise.allSettled([p1, p2]);
Abortable search
Debounce input.
Abort previous controller before starting new fetch.