Step-by-step
-
1
Make a basic fetch call
Call
fetch(url), which returns a Promise that resolves to aResponseobject. Callresponse.json()to parse the body — this also returns a Promise. Useawaitfor both.javascriptasync function getUser() { const response = await fetch('https://api.github.com/users/octocat'); const data = await response.json(); console.log(data); } getUser(); -
2
Check response.ok — fetch does not throw on errors
If the server returns a 404 or 500,
fetchstill resolves. The Promise only rejects on network failures (no connection, DNS error, CORS block). You must checkresponse.ok(true for 200–299) orresponse.statusexplicitly, then throw your own error.javascriptasync function getUser(username) { const response = await fetch(`https://api.github.com/users/${username}`); if (!response.ok) { throw new Error(`HTTP error: ${response.status} ${response.statusText}`); } return response.json(); } -
3
Wrap everything in try/catch
Network errors, JSON parse failures, and the explicit
throwfrom the previous step all surface as rejected Promises. A singletry/catchblock handles all of them in one place.javascriptasync function getUser(username) { try { const response = await fetch(`https://api.github.com/users/${username}`); if (!response.ok) throw new Error(`HTTP ${response.status}`); const data = await response.json(); renderUser(data); } catch (err) { showError(err.message); } } -
4
Show loading and error states
Set a loading indicator before the request, hide it after. On error, show a message in the UI — never silently swallow errors. Keep the error element in your HTML but hide it by default.
javascriptconst output = document.getElementById('output'); const errorEl = document.getElementById('error'); const loader = document.getElementById('loader'); async function getUser(username) { loader.hidden = false; errorEl.hidden = true; output.hidden = true; try { const response = await fetch(`https://api.github.com/users/${username}`); if (!response.ok) throw new Error(`HTTP ${response.status}`); const data = await response.json(); renderUser(data); output.hidden = false; } catch (err) { errorEl.textContent = `Failed to load: ${err.message}`; errorEl.hidden = false; } finally { loader.hidden = true; } } -
5
Render data safely into the DOM
Use
textContentfor any user-supplied or API-sourced string. It escapes HTML automatically —innerHTMLdoes not, and could execute a script tag if the API returns malicious data. Only useinnerHTMLwhen you control 100% of the content.javascriptfunction renderUser(data) { const card = document.getElementById('user-card'); // Safe: textContent escapes HTML entities card.querySelector('.name').textContent = data.name ?? data.login; card.querySelector('.bio').textContent = data.bio ?? 'No bio.'; card.querySelector('.repos').textContent = `${data.public_repos} repositories`; // Safe for URLs you trust (keep validation tight in real apps) const avatar = card.querySelector('.avatar'); avatar.src = data.avatar_url; avatar.alt = `Avatar of ${data.login}`; } -
6
Cancel stale requests with AbortController
When a user types a new search query before the previous one completes, the old response should not replace the new one.
AbortControllerlets you cancel in-flight requests. Create a new controller for each call and abort the previous one.javascriptlet currentController = null; async function searchUsers(query) { // Abort any in-flight request if (currentController) currentController.abort(); currentController = new AbortController(); try { const response = await fetch( `https://api.github.com/search/users?q=${encodeURIComponent(query)}`, { signal: currentController.signal } ); if (!response.ok) throw new Error(`HTTP ${response.status}`); const data = await response.json(); renderResults(data.items); } catch (err) { if (err.name === 'AbortError') return; // Expected — ignore showError(err.message); } } -
7
Send POST requests with JSON body
For POST/PUT/PATCH requests, pass a second options object to
fetch. Stringify the body, and setContent-Type: application/jsonso the server parses it correctly.javascriptasync function createRepo(name, isPrivate = false) { const response = await fetch('https://api.github.com/user/repos', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}`, }, body: JSON.stringify({ name, private: isPrivate }), }); if (!response.ok) { const err = await response.json(); throw new Error(err.message ?? `HTTP ${response.status}`); } return response.json(); }
Tips & gotchas
- Always check <code>response.ok</code> before calling <code>response.json()</code> — a 4xx/5xx body might not be valid JSON.
- Use <code>encodeURIComponent()</code> on any query parameter derived from user input to prevent URL injection.
- The browser caches GET requests aggressively. Append a cache-busting param or use <code>cache: 'no-store'</code> in the fetch options when you always need fresh data.
- For multiple concurrent requests, <code>Promise.all([fetch(a), fetch(b)])</code> runs them in parallel and waits for both.
- Avoid re-fetching data that hasn't changed. A simple in-memory Map keyed by URL is a lightweight client-side cache.
Wrapping up
The fetch API covers everything you need for HTTP requests in the browser, but you have to use it correctly. The key habits: check response.ok, wrap with try/catch, render with textContent, and cancel with AbortController. Get these four right and you won't need Axios for most projects.