Sunday, August 31, 2025

Javascript Module 59

  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.


No comments:

Post a Comment

Javascript Module 78

  Javascript   Module 78 If You want To Earn Certificate For My  All   Course Then Contact Me At My  Contact  Page   then I Will Take A Test...