| |

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:

MethodReturns
.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:

OptionPurpose
methodHTTP verb
headersRequest headers
bodyRequest body
signalAbortController signal
credentialsCookie/header handling
modeCORS mode
cacheCaching 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:

APILifetimeScope
localStorageForever (until cleared)Origin
sessionStorageUntil tab closesOrigin + tab
CookiesConfigurableDomain + 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:

FunctionRuns
setTimeoutOnce, after a delay
setIntervalRepeatedly, 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 setInterval or 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

MethodPurpose
fetch(url)GET request
fetch(url, options)Custom request
response.json()Parse JSON
response.text()Text
response.blob()Binary
response.ok200–299
response.statusHTTP status
response.headersHeaders

fetch Options

OptionPurpose
methodHTTP verb
headersRequest headers
bodyRequest body
signalAbortController
credentialsCookies
modeCORS

localStorage

MethodPurpose
setItem(k, v)Store
getItem(k)Read
removeItem(k)Delete
clear()Clear all
key(i)Nth key
lengthCount

Storage Comparison

FeaturelocalStoragesessionStorageCookies
LifetimeForeverTab closeConfigurable
ScopeOriginOrigin + tabDomain
Size~5MB~5MB~4KB
Sent to server
Sync

setTimeout

MethodPurpose
setTimeout(fn, ms, ...)Schedule once
clearTimeout(id)Cancel
setInterval(fn, ms)Repeat
clearInterval(id)Stop repeat

Timing Patterns

PatternUse
delay(ms)Async pause
DebounceWait for pause
ThrottleRate limit
Recursive timeoutAvoid overlap
BackoffRetry 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

PitfallProblemSolution
fetch on 404Doesn’t throwCheck res.ok
Body read twiceThrowsRead once
Object in localStorage[object Object]JSON.stringify
Quota exceededThrowsHandle error
Storage in private modeThrowsDetect + fallback
Delays in background tabsThrottledDon’t rely on precision
Recursive setTimeout leakGrowsClear on unmount
setInterval overlapConcurrent callsUse recursive setTimeout
Storing token in localStorageXSS riskUse httpOnly cookie
Awaiting fetch without try/catchUnhandled rejectionWrap 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

ConceptAPIExample
HTTP requestfetch(url)await fetch('/api')
Parse JSONresponse.json()await res.json()
Check statusresponse.okif (!res.ok)
Send datafetch(url, opts)method: 'POST'
Cancel requestAbortController{ signal }
Store stringlocalStorage.setItemsetItem('k', 'v')
ReadlocalStorage.getItemgetItem('k')
Store objectJSON.stringifysetItem('k', JSON.stringify(o))
Persist per tabsessionStorageCleared on close
Sync tabsstorage eventaddEventListener
Delay oncesetTimeoutsetTimeout(fn, 1000)
RepeatsetIntervalsetInterval(fn, 1000)
CancelclearTimeoutclearTimeout(id)
Promise delaywrapperdelay(500)
Debouncehelperdebounce(fn, 300)

Key takeaways:

  • fetch returns a Promise that resolves to a Response, not the data
  • You must call .json() or .text() to read the body — a second Promise
  • fetch doesn’t reject on HTTP errors — check response.ok or response.status
  • Use AbortController for timeouts and cancellations
  • localStorage is synchronous, string-only, and per-origin
  • Always serialize objects with JSON.stringify and parse with JSON.parse
  • sessionStorage clears when the tab closes; localStorage persists
  • storage events sync changes across tabs on the same origin
  • Never store secrets in localStorage — XSS can read them
  • setTimeout runs once after a minimum delay — never exactly on time
  • setInterval can overlap slow async work — prefer recursive setTimeout
  • Wrap setTimeout in a Promise for use with async/await
  • Debounce and throttle are timer-based patterns for high-frequency events
  • Combine the three: localStorage persists, fetch sends, setTimeout schedules

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!