Browser Web APIs in JavaScript
A practical guide to the most useful browser Web APIs — Fetch, localStorage, Intersection Observer, Web Workers, WebSockets, Geolocation, and the Clipboard API.
Modern browsers expose a rich set of APIs that go far beyond the DOM. This guide covers the ones you’ll reach for most often in production applications — how to use them correctly, and the gotchas that trip up developers the first time.
Fetch API
fetch is the modern replacement for XMLHttpRequest. It returns a Promise and integrates cleanly with async/await, making HTTP requests straightforward to write and read. The critical thing to know upfront: Fetch does not reject on HTTP error status codes. A 404 or 500 still resolves the Promise — you must check response.ok yourself and throw if the request failed. Building a reusable wrapper prevents this mistake from appearing throughout your codebase.
// A reusable fetch wrapper with proper error handling
async function apiFetch(url, options = {}) {
const defaultOptions = {
headers: {
"Content-Type": "application/json",
...options.headers,
},
};
const response = await fetch(url, { ...defaultOptions, ...options });
if (!response.ok) {
// Include status in the error so callers can branch on it
const error = new Error(`HTTP ${response.status}: ${response.statusText}`);
error.status = response.status;
throw error;
}
// Handle 204 No Content — response.json() would throw on an empty body
if (response.status === 204) return null;
return response.json();
}
// POST example
async function createUser(userData) {
try {
const user = await apiFetch("/api/users", {
method: "POST",
body: JSON.stringify(userData),
});
console.log("Created:", user);
return user;
} catch (err) {
if (err.status === 409) {
console.error("User already exists");
} else {
console.error("Unexpected error:", err.message);
}
throw err;
}
}
For request cancellation, use AbortController. This is especially important for search-as-you-type features — you want to cancel the previous request when the user types another character:
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000); // 5s timeout
try {
const data = await apiFetch("/api/slow-endpoint", {
signal: controller.signal,
});
clearTimeout(timeoutId);
return data;
} catch (err) {
if (err.name === "AbortError") {
console.warn("Request timed out");
}
}
localStorage and sessionStorage
localStorage and sessionStorage let you persist data in the browser without a server round-trip — useful for user preferences, cached API responses, or form drafts. Both APIs only store strings, so you need to serialize and parse JSON. A thin wrapper centralizes the serialization logic and handles the QuotaExceededError that occurs when storage is full or the user is in private mode on some browsers.
const storage = {
get(key, fallback = null) {
try {
const item = localStorage.getItem(key);
// getItem returns null for missing keys — distinguish from stored null
return item !== null ? JSON.parse(item) : fallback;
} catch {
return fallback; // JSON.parse failed or localStorage unavailable
}
},
set(key, value) {
try {
localStorage.setItem(key, JSON.stringify(value));
return true;
} catch (err) {
// QuotaExceededError: storage full, or private mode in some browsers
console.warn("localStorage.set failed:", err);
return false;
}
},
remove(key) {
localStorage.removeItem(key);
},
clear() {
localStorage.clear();
},
};
// Usage
storage.set("user", { id: 1, name: "Alice", theme: "dark" });
const user = storage.get("user"); // { id: 1, name: 'Alice', theme: 'dark' }
const missing = storage.get("nope", {}); // {} — fallback returned
Use sessionStorage the same way — swap localStorage for sessionStorage in the wrapper.
Intersection Observer
Scroll event listeners are expensive — they fire on every pixel scrolled and force layout recalculations. IntersectionObserver is the browser-native alternative: it fires a callback asynchronously when elements enter or exit the viewport, with no scroll listener required. This makes it the right tool for lazy loading images, triggering animations when elements come into view, and implementing infinite scroll.
// Lazy-load images: use data-src, set src only when image enters the viewport
function lazyLoadImages() {
const observer = new IntersectionObserver(
(entries, obs) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) return;
const img = entry.target;
img.src = img.dataset.src; // trigger the actual download
img.removeAttribute("data-src"); // clean up the data attribute
obs.unobserve(img); // stop watching — no need to fire again
});
},
{
rootMargin: "200px", // start loading 200px before the image enters the viewport
threshold: 0, // fire as soon as any part of the image is visible
}
);
document.querySelectorAll("img[data-src]").forEach((img) => {
observer.observe(img);
});
return observer; // keep a reference so you can call disconnect() when done
}
// HTML: <img data-src="/photo.jpg" alt="Photo" width="800" height="600">
lazyLoadImages();
Web Workers
JavaScript is single-threaded — any CPU-heavy computation blocks the main thread, freezing the UI. Web Workers solve this by running scripts on a background thread. The tradeoff is that workers have no access to the DOM; they communicate with the main thread exclusively via postMessage. This isolation is actually a feature — it prevents workers from accidentally causing race conditions on the UI.
// worker.js — runs in its own thread, completely isolated from the DOM
self.onmessage = function (e) {
const { numbers } = e.data;
// CPU-heavy work: sieve of Eratosthenes to find all primes up to `numbers`
const primes = [];
for (let n = 2; n <= numbers; n++) {
let isPrime = true;
for (let i = 2; i <= Math.sqrt(n); i++) {
if (n % i === 0) { isPrime = false; break; }
}
if (isPrime) primes.push(n);
}
// Send results back to the main thread
self.postMessage({ primes, count: primes.length });
};
// main.js — UI thread stays responsive the entire time
const worker = new Worker("/worker.js");
worker.onmessage = (e) => {
console.log(`Found ${e.data.count} primes`);
worker.terminate(); // free up the background thread when done
};
worker.onerror = (err) => {
console.error("Worker error:", err.message);
};
worker.postMessage({ numbers: 1_000_000 }); // UI stays fully interactive
WebSockets
HTTP is request-response — the client asks, the server answers, then the connection closes. WebSockets provide a persistent, full-duplex connection where either side can send data at any time. This makes them ideal for chat, live dashboards, collaborative editing, and multiplayer games. In production, you almost always need reconnection logic since connections can drop.
class ReconnectingWebSocket {
constructor(url) {
this.url = url;
this.ws = null;
this.reconnectDelay = 1000;
this.connect();
}
connect() {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
console.log("Connected");
this.reconnectDelay = 1000; // reset exponential backoff on successful connect
};
this.ws.onmessage = (e) => {
const data = JSON.parse(e.data);
this.onMessage(data);
};
this.ws.onclose = () => {
console.warn(`Disconnected. Reconnecting in ${this.reconnectDelay}ms`);
setTimeout(() => this.connect(), this.reconnectDelay);
// Exponential backoff — avoid hammering the server on repeated failures
this.reconnectDelay = Math.min(this.reconnectDelay * 2, 30_000);
};
}
send(data) {
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(data));
}
}
onMessage(data) {
// Override in subclass or reassign as a property
console.log("Message:", data);
}
}
Geolocation API
The Geolocation API provides the device’s physical location, but it requires user permission and may not be available in all environments. Wrapping the callback-based API in a Promise makes it easier to use with async/await and lets you handle the permission-denied case cleanly.
async function getCurrentPosition(options = {}) {
return new Promise((resolve, reject) => {
if (!navigator.geolocation) {
reject(new Error("Geolocation not supported"));
return;
}
navigator.geolocation.getCurrentPosition(resolve, reject, {
enableHighAccuracy: false, // true uses GPS but drains battery faster
timeout: 10_000, // fail if no position within 10 seconds
maximumAge: 60_000, // accept a cached position up to 1 minute old
...options,
});
});
}
try {
const position = await getCurrentPosition();
const { latitude, longitude, accuracy } = position.coords;
console.log(`Location: ${latitude}, ${longitude} (±${accuracy}m)`);
} catch (err) {
if (err.code === GeolocationPositionError.PERMISSION_DENIED) {
console.error("User denied location access");
}
}
Clipboard API
The Clipboard API provides programmatic access to the system clipboard — essential for “Copy to clipboard” buttons. It requires a user gesture (a click) and HTTPS in most browsers. The fallback using document.execCommand covers older browsers where the Clipboard API is not available.
// Write to clipboard — requires a user gesture or HTTPS
async function copyToClipboard(text) {
try {
await navigator.clipboard.writeText(text);
return true;
} catch {
// Fallback for older browsers using the deprecated execCommand API
const el = document.createElement("textarea");
el.value = text;
el.style.position = "fixed";
el.style.opacity = "0";
document.body.appendChild(el);
el.select();
const success = document.execCommand("copy");
document.body.removeChild(el);
return success;
}
}
// Read from clipboard — requires clipboard-read permission prompt
async function readFromClipboard() {
try {
return await navigator.clipboard.readText();
} catch {
return null; // permission denied or API not available
}
}
Common Pitfalls
- CORS:
fetchrespects CORS. If the server doesn’t send the right headers, the browser blocks the response — the error appears in DevTools Network, not in your catch block as a useful message. - localStorage size: The limit is ~5MB per origin. For large data, use IndexedDB.
- Web Workers can’t share state: Pass data via
postMessage. For large buffers (images, audio), useTransferableobjects to avoid copying:worker.postMessage({ buffer }, [buffer]). - WebSocket binary: Use
ws.binaryType = 'arraybuffer'before connecting if you expect binary frames.