JavaScript 63 🧬 Browser APIs — fetch, localStorage, setTimeout
The browser gives JavaScript more than just the DOM. It exposes a set of Web APIs — tools the environment provides on top of the language — for talking to servers, storing data, and scheduling work. Three of them cover the vast majority of what you’ll do in real code: fetch for network requests, localStorage for persistence, and setTimeout for timing.
These aren’t part of JavaScript itself. They’re provided by the browser (and in some cases, Node.js). That’s why you can’t fetch in an old script engine, but you can in every modern browser and in Node since v18.
Key point: These APIs are asynchronous or persistent — they don’t run inside the normal synchronous flow. fetch returns a Promise, setTimeout schedules a callback, and localStorage writes to disk. Understanding when each one runs (or persists) is what makes them useful instead of surprising.
a – fetch — talking to servers
fetch is the modern way to make HTTP requests. It replaces the older XMLHttpRequest with a Promise-based API that reads cleanly and works with async/await.
Basic usage:
const response = await fetch('https://api.example.com/users');
const data = await response.json();
console.log(data);
fetch returns a Promise that resolves to a Response object. That response isn’t the data — it’s a wrapper. You call .json(), .text(), .blob(), or .arrayBuffer() to read the body, and each of those returns another Promise.
Why two awaits? Because there are two asynchronous steps: the request going out and the body coming back. fetch resolves once headers arrive; the body may still be streaming. Reading the body is the second Promise.
A POST request:
const response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'Alice', age: 30 })
});
if (!response.ok) throw new Error('Request failed');
const result = await response.json();
The second argument is an options object. method, headers, and body are the three you’ll set most often.
Checking for errors:
fetch does not reject on HTTP errors like 404 or 500. It only rejects on network failures — DNS lookup failed, no internet, CORS blocked. So you have to check response.ok or response.status yourself.
const response = await fetch('/api/missing');
if (!response.ok) {
console.log(response.status); // 404
console.log(response.statusText); // 'Not Found'
}
This is the single most common fetch gotcha. A 404 is a “successful” fetch in the Promise sense.
Timeout with AbortController:
fetch has no built-in timeout. You use AbortController to cancel a slow request:
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);
try {
const response = await fetch('/slow', { signal: controller.signal });
const data = await response.json();
} catch (err) {
if (err.name === 'AbortError') console.log('Timed out');
} finally {
clearTimeout(timeout);
}
Reading different body types:
| Method | Returns |
|---|---|
.json() | Parsed JSON object |
.text() | Raw string |
.blob() | Binary data as Blob |
.arrayBuffer() | Raw bytes |
.formData() | FormData object |
You can only read the body once. Calling .json() and then .text() throws.
Common fetch options:
| Option | Purpose |
|---|---|
method | HTTP verb |
headers | Request headers |
body | Request body |
signal | AbortController signal |
credentials | Cookie/header handling |
mode | CORS mode |
cache | Caching strategy |
When to use fetch:
- Calling REST APIs
- Submitting forms
- Downloading files
- Uploading data
- Anything that talks to a server
When not to use fetch:
- For simple
<a href>navigation — let the browser do it - For form submission with a page reload — use a plain
<form> - For WebSocket connections — use
WebSocket - In very old browsers — use a polyfill or
XMLHttpRequest
b – localStorage — persisting data
localStorage stores key-value pairs in the browser that survive page reloads, tab closes, and browser restarts. It’s synchronous, string-only, and per-origin.
Basic usage:
localStorage.setItem('theme', 'dark');
const theme = localStorage.getItem('theme');
localStorage.removeItem('theme');
localStorage.clear();
Everything is a string. If you store an object, it gets converted to "[object Object]" — the classic mistake:
localStorage.setItem('user', { name: 'Alice' }); // ❌
localStorage.getItem('user'); // '[object Object]'
Storing objects — the right way:
localStorage.setItem('user', JSON.stringify({ name: 'Alice', age: 30 }));
const user = JSON.parse(localStorage.getItem('user'));
Serialize with JSON.stringify on the way in, deserialize with JSON.parse on the way out.
The three storage APIs:
| API | Lifetime | Scope |
|---|---|---|
localStorage | Forever (until cleared) | Origin |
sessionStorage | Until tab closes | Origin + tab |
| Cookies | Configurable | Domain + path |
sessionStorage works the same way but is cleared when the tab closes:
sessionStorage.setItem('cart', JSON.stringify(cart));
Limits:
- Roughly 5–10MB per origin (varies by browser)
- Synchronous — blocks the main thread for large writes
- String only — everything is coerced
- Per-origin — different subdomains don’t share
Storage events — syncing across tabs:
When one tab writes to localStorage, other tabs on the same origin receive a storage event:
window.addEventListener('storage', (event) => {
console.log(event.key, event.oldValue, event.newValue);
});
This is how apps sync state across open tabs — login/logout, theme changes, cart updates.
Checking availability:
localStorage throws in some contexts — private browsing on old Safari, when cookies are blocked, or when the quota is exceeded:
function storageAvailable() {
try {
const test = '__test__';
localStorage.setItem(test, test);
localStorage.removeItem(test);
return true;
} catch {
return false;
}
}
Quota errors:
Writes can throw QuotaExceededError when storage is full:
try {
localStorage.setItem('big', hugeString);
} catch (err) {
if (err.name === 'QuotaExceededError') {
console.log('Storage full');
}
}
Common localStorage uses:
- Theme preference (light/dark)
- Language setting
- Auth token (⚠️ risky — XSS can steal it)
- Form draft persistence
- UI state (collapsed panels, sort order)
- Cached data for offline use
When not to use localStorage:
- Sensitive data — it’s accessible to any script on the page. Tokens stored here can be stolen by XSS.
- Large data — 5MB limit; use IndexedDB instead.
- Complex queries — no indexing; use IndexedDB or a backend.
- Anything critical — user can clear it, browser can evict it.
c – setTimeout — scheduling work
setTimeout runs a function once, after at least a given number of milliseconds. It doesn’t block — the rest of the code keeps running. The function goes into the event loop queue and runs when its timer expires.
Basic usage:
setTimeout(() => {
console.log('after 1 second');
}, 1000);
The first argument is the callback. The second is the delay in milliseconds. Any further arguments are passed to the callback:
setTimeout((name, age) => {
console.log(`${name} is ${age}`);
}, 1000, 'Alice', 30);
The delay is a minimum, not a guarantee:
const start = Date.now();
setTimeout(() => {
console.log(Date.now() - start);
}, 0);
// [ 1 ] or [ 2 ] or more
A setTimeout(0) doesn’t run immediately. The callback waits until the current synchronous code finishes, then runs as a macrotask. In practice, delays are usually 1–4ms even for 0.
Cancelling:
const id = setTimeout(() => console.log('never'), 1000);
clearTimeout(id);
setTimeout returns a numeric ID. Pass it to clearTimeout to cancel.
setTimeout vs setInterval:
| Function | Runs |
|---|---|
setTimeout | Once, after a delay |
setInterval | Repeatedly, every N ms |
const id = setInterval(() => {
console.log('tick');
}, 1000);
clearInterval(id); // stop
setInterval fires on schedule even if the previous callback hasn’t finished — which can cause overlapping work. If your callback is async or slow, prefer a recursive setTimeout:
async function poll() {
await fetchData();
setTimeout(poll, 5000);
}
poll();
This waits 5 seconds after each call completes, avoiding overlap.
Delaying with a Promise:
setTimeout is the classic way to pause an async function:
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
await delay(1000);
console.log('after 1 second');
This is used everywhere — debouncing, retrying, rate limiting, animation steps.
Debounce:
function debounce(fn, wait) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), wait);
};
}
Each call cancels the previous timer. The function runs only after wait ms of silence — perfect for search-as-you-type.
Throttle (with timers):
function throttle(fn, limit) {
let inThrottle = false;
return (...args) => {
if (!inThrottle) {
fn(...args);
inThrottle = true;
setTimeout(() => (inThrottle = false), limit);
}
};
}
Runs at most once every limit ms. Used for scroll and resize handlers.
When to use setTimeout:
- Delaying a single action
- Polling with backoff
- Debouncing input
- Throttling rapid events
- Yielding the event loop between heavy tasks
- Simple animations (better:
requestAnimationFrame)
When not to use setTimeout:
- Precise timing — the browser doesn’t guarantee millisecond accuracy
- Animations — use
requestAnimationFrame - Recurring work — consider
setIntervalor an async loop - Long delays in background tabs — browsers throttle to ~1 minute
Timers in background tabs:
Browsers throttle timers in inactive tabs — often to once per second or once per minute. Don’t rely on them for precise scheduling in hidden tabs.
Combining all three:
A realistic example — save a form draft locally, fetch saved values on load, and debounce the save.
const form = document.querySelector('#form');
const draftKey = 'draft';
// Restore on load
const saved = localStorage.getItem(draftKey);
if (saved) {
const data = JSON.parse(saved);
Object.entries(data).forEach(([name, value]) => {
if (form.elements[name]) form.elements[name].value = value;
});
}
// Save on input, debounced
const save = debounce(() => {
const data = Object.fromEntries(new FormData(form));
localStorage.setItem(draftKey, JSON.stringify(data));
}, 500);
form.addEventListener('input', save);
// Submit to server
form.addEventListener('submit', async (event) => {
event.preventDefault();
const response = await fetch('/api/save', {
method: 'POST',
body: new FormData(form)
});
if (response.ok) {
localStorage.removeItem(draftKey);
}
});
Each API plays its role: localStorage persists, fetch sends, setTimeout debounces.
Complete Example Session
// ============================================
// PART 1: BASIC FETCH
// ============================================
const response = await fetch('https://api.example.com/data');
const data = await response.json();
console.log(data);
// [ { ...server response... } ]
// ============================================
// PART 2: FETCH WITH ERROR CHECK
// ============================================
const res = await fetch('/api/missing');
if (!res.ok) {
console.log('HTTP error:', res.status);
// [ HTTP error: 404 ]
}
// ============================================
// PART 3: POST WITH JSON
// ============================================
await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'Alice' })
});
// ============================================
// PART 4: TIMEOUT WITH ABORTCONTROLLER
// ============================================
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 3000);
try {
await fetch('/slow-endpoint', { signal: controller.signal });
} catch (err) {
if (err.name === 'AbortError') console.log('Request timed out');
} finally {
clearTimeout(timer);
}
// ============================================
// PART 5: LOCALSTORAGE BASICS
// ============================================
localStorage.setItem('theme', 'dark');
console.log(localStorage.getItem('theme'));
// [ 'dark' ]
localStorage.removeItem('theme');
// ============================================
// PART 6: STORING OBJECTS
// ============================================
localStorage.setItem('user', JSON.stringify({ name: 'Alice' }));
const user = JSON.parse(localStorage.getItem('user'));
console.log(user.name);
// [ 'Alice' ]
// ============================================
// PART 7: SESSIONSTORAGE
// ============================================
sessionStorage.setItem('tab', 'profile');
console.log(sessionStorage.getItem('tab'));
// [ 'profile' ]
// Cleared when the tab closes
// ============================================
// PART 8: STORAGE EVENT
// ============================================
window.addEventListener('storage', (event) => {
console.log('changed:', event.key, '→', event.newValue);
});
// Fires in other tabs on the same origin
// ============================================
// PART 9: SETTIMEOUT BASICS
// ============================================
setTimeout(() => {
console.log('delayed');
}, 1000);
// [ delayed ] after ~1s
// ============================================
// PART 10: CLEARING TIMEOUT
// ============================================
const id = setTimeout(() => console.log('never'), 5000);
clearTimeout(id);
// No output
// ============================================
// PART 11: DELAY HELPER
// ============================================
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
await delay(500);
console.log('after 500ms');
// [ after 500ms ]
// ============================================
// PART 12: DEBOUNCE
// ============================================
function debounce(fn, wait) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), wait);
};
}
const search = debounce(q => console.log('Search:', q), 300);
search('a');
search('ab');
search('abc');
// Only 'abc' logs, 300ms later
// [ Search: abc ]
// ============================================
// PART 13: RECURSIVE POLLING
// ============================================
async function poll() {
console.log('polling...');
await delay(1000);
// poll(); — would loop forever
}
poll();
// [ polling... ]
// ============================================
// PART 14: COMBINED PATTERN
// ============================================
async function loadCachedOrFetch(key, url) {
const cached = localStorage.getItem(key);
if (cached) return JSON.parse(cached);
const response = await fetch(url);
const data = await response.json();
localStorage.setItem(key, JSON.stringify(data));
return data;
}
const users = await loadCachedOrFetch('users', '/api/users');
// Returns from cache if present, otherwise fetches and caches
// ============================================
// PART 15: FETCH WITH RETRY
// ============================================
async function fetchWithRetry(url, attempts = 3) {
for (let i = 0; i < attempts; i++) {
try {
const res = await fetch(url);
if (res.ok) return await res.json();
} catch (err) {
if (i === attempts - 1) throw err;
await delay(1000 * 2 ** i);
}
}
throw new Error('All attempts failed');
}
const result = await fetchWithRetry('/api/flaky');
// Retries with exponential backoff
Quick Reference
fetch
| Method | Purpose |
|---|---|
fetch(url) | GET request |
fetch(url, options) | Custom request |
response.json() | Parse JSON |
response.text() | Text |
response.blob() | Binary |
response.ok | 200–299 |
response.status | HTTP status |
response.headers | Headers |
fetch Options
| Option | Purpose |
|---|---|
method | HTTP verb |
headers | Request headers |
body | Request body |
signal | AbortController |
credentials | Cookies |
mode | CORS |
localStorage
| Method | Purpose |
|---|---|
setItem(k, v) | Store |
getItem(k) | Read |
removeItem(k) | Delete |
clear() | Clear all |
key(i) | Nth key |
length | Count |
Storage Comparison
| Feature | localStorage | sessionStorage | Cookies |
|---|---|---|---|
| Lifetime | Forever | Tab close | Configurable |
| Scope | Origin | Origin + tab | Domain |
| Size | ~5MB | ~5MB | ~4KB |
| Sent to server | ❌ | ❌ | ✅ |
| Sync | ✅ | ✅ | ✅ |
setTimeout
| Method | Purpose |
|---|---|
setTimeout(fn, ms, ...) | Schedule once |
clearTimeout(id) | Cancel |
setInterval(fn, ms) | Repeat |
clearInterval(id) | Stop repeat |
Timing Patterns
| Pattern | Use |
|---|---|
delay(ms) | Async pause |
| Debounce | Wait for pause |
| Throttle | Rate limit |
| Recursive timeout | Avoid overlap |
| Backoff | Retry with increasing delay |
Best Practices
✅ Do This:
// Check response.ok before parsing
const res = await fetch(url);
if (!res.ok) throw new Error(res.statusText);
const data = await res.json();
// Serialize objects
localStorage.setItem('user', JSON.stringify(user));
const user = JSON.parse(localStorage.getItem('user'));
// Wrap setTimeout in a Promise for async
const delay = ms => new Promise(r => setTimeout(r, ms));
await delay(500);
// Use AbortController for timeouts
const c = new AbortController();
setTimeout(() => c.abort(), 5000);
await fetch(url, { signal: c.signal });
// Debounce input handlers
input.addEventListener('input', debounce(handler, 300));
// Clear intervals when done
const id = setInterval(fn, 1000);
clearInterval(id);
❌ Don’t Do This:
// Don't assume fetch rejects on HTTP errors
const res = await fetch('/missing');
const data = await res.json(); // ❌ 404 body
// Don't store objects directly
localStorage.setItem('user', user); // ❌
localStorage.setItem('user', JSON.stringify(user)); // ✅
// Don't store secrets in localStorage
localStorage.setItem('token', apiKey); // ❌ XSS risk
// Don't rely on exact timing
setTimeout(fn, 0); // ⚠️ 1–4ms
// Don't use setInterval for async work
setInterval(async () => await task(), 1000); // ⚠️ overlaps
// Don't forget clearTimeout in cleanup
const id = setTimeout(fn, 5000); // ⚠️ leaks
// Don't use localStorage in SSR or Node
localStorage.setItem('x', '1'); // ❌ not defined
Common Pitfalls
| Pitfall | Problem | Solution |
|---|---|---|
fetch on 404 | Doesn’t throw | Check res.ok |
| Body read twice | Throws | Read once |
| Object in localStorage | [object Object] | JSON.stringify |
| Quota exceeded | Throws | Handle error |
| Storage in private mode | Throws | Detect + fallback |
| Delays in background tabs | Throttled | Don’t rely on precision |
| Recursive setTimeout leak | Grows | Clear on unmount |
setInterval overlap | Concurrent calls | Use recursive setTimeout |
| Storing token in localStorage | XSS risk | Use httpOnly cookie |
| Awaiting fetch without try/catch | Unhandled rejection | Wrap in try |
Real-World Examples
1. Load data from an API
const res = await fetch('/api/users');
if (!res.ok) throw new Error('Failed');
const users = await res.json();
The standard request: fetch, check status, parse JSON.
2. POST a form
await fetch('/api/submit', {
method: 'POST',
body: new FormData(form)
});
Send form data with the appropriate Content-Type set by FormData.
3. Timeout a slow request
const c = new AbortController();
setTimeout(() => c.abort(), 5000);
try {
await fetch('/slow', { signal: c.signal });
} catch (err) {
console.log('Timed out');
}
Cancels after 5 seconds — the modern alternative to XMLHttpRequest.timeout.
4. Save theme preference
localStorage.setItem('theme', 'dark');
document.body.classList.toggle('dark',
localStorage.getItem('theme') === 'dark');
A classic use case — persist a small user choice across sessions.
5. Store an object
const user = { name: 'Alice', age: 30 };
localStorage.setItem('user', JSON.stringify(user));
const restored = JSON.parse(localStorage.getItem('user'));
Always serialize and deserialize.
6. Sync across tabs
window.addEventListener('storage', (event) => {
if (event.key === 'theme') updateTheme(event.newValue);
});
React to changes written by other tabs.
7. Debounce search
const search = debounce(query => {
fetch(`/api/search?q=${query}`);
}, 300);
input.addEventListener('input', e => search(e.target.value));
Wait for typing to pause before firing the request.
8. Async pause
const delay = ms => new Promise(r => setTimeout(r, ms));
await delay(1000);
Use this everywhere you need to pause an async function.
9. Retry with backoff
async function retry(fn, attempts = 3) {
for (let i = 0; i < attempts; i++) {
try { return await fn(); }
catch (err) {
if (i === attempts - 1) throw err;
await delay(2 ** i * 1000);
}
}
}
Waits 1s, 2s, 4s between retries.
10. Cache a fetch
async function getCached(key, url) {
const cached = localStorage.getItem(key);
if (cached) return JSON.parse(cached);
const data = await (await fetch(url)).json();
localStorage.setItem(key, JSON.stringify(data));
return data;
}
Simple cache layer over fetch — useful for stable data.
Visual: fetch Lifecycle
┌──────────────────────────────────────────────┐
│ fetch(url) │
│ │ │
│ ▼ │
│ Promise resolves to Response │
│ │ │
│ ├── .ok ──► true / false │
│ ├── .status ──► 200, 404, 500 │
│ │ │
│ ▼ │
│ response.json() ← second Promise │
│ │ │
│ ▼ │
│ Parsed data │
│ │
└──────────────────────────────────────────────┘
Visual: localStorage vs sessionStorage
┌──────────────────────────────────────────────┐
│ localStorage │
│ │
│ Tab A ──► writes key │
│ Tab B ──► sees same key │
│ Close tab → data stays │
│ Restart browser → data stays │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ sessionStorage │
│ │
│ Tab A ──► writes key │
│ Tab B ──► doesn't see it │
│ Close tab → data gone │
│ │
└──────────────────────────────────────────────┘
Visual: setTimeout in the event loop
┌──────────────────────────────────────────────┐
│ console.log('1'); │
│ setTimeout(() => log('2'), 0); │
│ console.log('3'); │
│ │
│ 1. '1' (sync) │
│ 2. '3' (sync) │
│ 3. schedule setTimeout callback │
│ 4. call stack empties │
│ 5. event loop runs the callback ──► '2' │
│ │
└──────────────────────────────────────────────┘
Summary
| Concept | API | Example |
|---|---|---|
| HTTP request | fetch(url) | await fetch('/api') |
| Parse JSON | response.json() | await res.json() |
| Check status | response.ok | if (!res.ok) |
| Send data | fetch(url, opts) | method: 'POST' |
| Cancel request | AbortController | { signal } |
| Store string | localStorage.setItem | setItem('k', 'v') |
| Read | localStorage.getItem | getItem('k') |
| Store object | JSON.stringify | setItem('k', JSON.stringify(o)) |
| Persist per tab | sessionStorage | Cleared on close |
| Sync tabs | storage event | addEventListener |
| Delay once | setTimeout | setTimeout(fn, 1000) |
| Repeat | setInterval | setInterval(fn, 1000) |
| Cancel | clearTimeout | clearTimeout(id) |
| Promise delay | wrapper | delay(500) |
| Debounce | helper | debounce(fn, 300) |
Key takeaways:
fetchreturns a Promise that resolves to aResponse, not the data- You must call
.json()or.text()to read the body — a second Promise fetchdoesn’t reject on HTTP errors — checkresponse.okorresponse.status- Use
AbortControllerfor timeouts and cancellations localStorageis synchronous, string-only, and per-origin- Always serialize objects with
JSON.stringifyand parse withJSON.parse sessionStorageclears when the tab closes;localStoragepersistsstorageevents sync changes across tabs on the same origin- Never store secrets in
localStorage— XSS can read them setTimeoutruns once after a minimum delay — never exactly on timesetIntervalcan overlap slow async work — prefer recursivesetTimeout- Wrap
setTimeoutin a Promise for use withasync/await - Debounce and throttle are timer-based patterns for high-frequency events
- Combine the three:
localStoragepersists,fetchsends,setTimeoutschedules
Remember: These three APIs cover most of what a front-end application needs. fetch for the network, localStorage for persistence, setTimeout for timing. They aren’t part of the language — they’re the browser environment — but they’re what makes JavaScript useful in the real world. Learn their quirks: fetch doesn’t reject on 404, localStorage is string-only, and setTimeout is always at least a few milliseconds late. Master these APIs, and your apps can talk to servers, remember state, and schedule work — all without a framework.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!