localStorage, Cookies, Fetch, AbortController
localStorage, sessionStorage
Web storage objects localStorage and sessionStorage allow saving key/value pairs in the browser.
localStoragepersists across browser restarts — data stays until explicitly removed.sessionStoragepersists within the tab session (including page refreshes) but clears when the tab or window closes.
Both are bound to the origin (domain/protocol/port triplet). Different origins cannot access each other's storage.
Unlike cookies:
- Web storage objects are not sent to the server with each request. Because of that, we can store much more. Most modern browsers allow at least 5 megabytes of data (or more).
- The server can't manipulate storage objects via HTTP headers. Everything's done in JavaScript.
Both storage objects provide the same methods and properties:
setItem(key, value)– store key/value pair.getItem(key)– get the value by key.removeItem(key)– remove the key with its value.clear()– delete everything.key(index)– get the key on a given position.length– the number of stored items.
// Storing a string
localStorage.setItem('theme', 'dark');
// Storing an object (requires JSON.stringify)
const user = { name: 'Alice', loggedIn: true };
localStorage.setItem('user', JSON.stringify(user));
// Retrieving and parsing the data
const savedTheme = localStorage.getItem('theme'); // 'dark'
const savedUser = JSON.parse(localStorage.getItem('user')); // { name: 'Alice', loggedIn: true }
console.log(savedTheme);
console.log(savedUser.name); // 'Alice'
// Removing an item
localStorage.removeItem('theme');
// Clear all data
localStorage.clear();
Security note: localStorage is accessible to any JavaScript running on the page. If your site is vulnerable to XSS, an attacker can read everything in localStorage. Never store highly sensitive data (passwords, unencrypted tokens) here without understanding the risk. In practice, many apps store JWTs in localStorage for convenience, but HttpOnly cookies are more secure.
Cookies
Cookies are the oldest web storage mechanism and the only one that's automatically sent with HTTP requests.
Cookies are small strings of data stored directly in the browser. They are usually set by a web server using the response Set-Cookie HTTP header. Then, the browser automatically adds them to (almost) every request to the same domain using the Cookie HTTP header.
alert( document.cookie ); // cookie1=value1; cookie2=value2;...
Split by ; to get separate cookies.
Writing to document.cookie
We can write to document.cookie. But it's not a data property, it's an accessor (getter/setter). An assignment to it is treated specially.
A write operation to document.cookie updates only the cookie mentioned in it and doesn't touch other cookies.
document.cookie = "user=John"; // update only cookie named 'user'
alert(document.cookie); // show all cookies
Cookie attributes
Cookies have many optional but important attributes:
document.cookie = "username=John Doe; expires=Thu, 18 Dec 2099 12:00:00 UTC; path=/; SameSite=Lax; Secure";
;path=path— URL path the cookie is accessible on. Default: current path.;domain=domain— Domain the cookie is accessible on. By default, only the exact domain that set it.;expires=date-in-UTCString-format— When the cookie expires. Without this, cookie is deleted when browser closes (session cookie).;max-age=seconds— Alternative to expires. Number of seconds until the cookie expires.;secure— Cookie is only sent over HTTPS. Important for any sensitive data.;samesite=lax/strict/none— CSRF protection. Controls whether cookies are sent with cross-site requests.Strict— never sent with cross-site requestsLax(browser default) — sent with top-level navigations but not with third-party requestsNone— sent with all requests (requiresSecureflag)
HttpOnly— Cookie is NOT accessible viadocument.cookie. Can only be set by the server viaSet-Cookieheader. This is the primary defense against XSS attacks stealing session cookies.
https://developer.mozilla.org/en-US/docs/Web/API/Document/cookie
Storage comparison
| Feature | localStorage | sessionStorage | Cookies | IndexedDB |
|---|---|---|---|---|
| Size limit | ~5-10 MB | ~5-10 MB | ~4 KB | Large (quota-based) |
| Persistence | Until cleared | Tab session | Until expiry | Until cleared |
| Sent with requests | No | No | Yes, every request | No |
| API | Simple key-value | Simple key-value | String-based | Structured, transactional |
| Best for | User preferences, cached data | Temporary tab state | Authentication, server state | Large data, offline apps |
For large structured data (offline apps, PWAs), see IndexedDB. Covered after TypeScript since the idb library uses TypeScript interfaces.
Fetch API
The Fetch API is the modern replacement for XMLHttpRequest. It provides a promise-based interface for making HTTP requests.
With the Fetch API, you make a request by calling fetch(), which is available as a global function in both window and worker contexts.
You pass it a URL string (or a Request object), along with an optional argument to configure the request.
The fetch() function returns a Promise which is fulfilled with a Response object representing the server's response. You can then check the request status and extract the body of the response in various formats, including text and JSON, by calling the appropriate method on the response.
Important:
fetch()only rejects on network failures (DNS errors, offline, etc.). HTTP error responses (404, 500) still resolve normally — you must checkresponse.okorresponse.statusyourself. This is a common gotcha.
async function getData() {
const url = "https://example.org/products.json";
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Response status: ${response.status}`);
}
const result = await response.json();
console.log(result);
} catch (error) {
console.error(error.message);
}
}
Request options
The first parameter of fetch() is the URL (or a Request object). The second parameter is an optional options object.
Options include:
method— GET, POST, DELETE, PUT, etc.headers— Custom headers for the request (Content-Type, Authorization, etc.)body— Request body for methods like POST or PUT. Can be JSON string, FormData, plain text, etc.mode— Controls CORS behavior.cache— How the browser handles caching.credentials— Whether to send cookies (see below).signal— An AbortSignal to cancel the request.
Sending cookies and authentication
By default, fetch() sends cookies only to same-origin requests. The credentials option controls this:
// Default — send cookies to same-origin only
fetch(url, { credentials: 'same-origin' });
// Send cookies to all origins (requires CORS on server)
fetch(url, { credentials: 'include' });
// Never send cookies
fetch(url, { credentials: 'omit' });
Cross-Origin Requests and CORS
CORS (Cross-Origin Resource Sharing) is a security mechanism that restricts which resources a web page can request from a different domain. The server must include the appropriate headers (e.g., Access-Control-Allow-Origin) to allow your origin to access the resource.
Response
Once you receive the response, you can process it in various ways. Common methods available on the response object include:
response.json()— parses the response body as JSON and returns a promise that resolves to a JavaScript object.response.text()— returns a promise that resolves to the response body as plain text.response.blob()— binary data (images, files).response.arrayBuffer()— raw binary buffer.
POST with JSON (using async/await)
async function createPost() {
const response = await fetch("https://jsonplaceholder.typicode.com/posts", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
title: "foo",
body: "bar",
userId: 1,
}),
});
const result = await response.json();
console.log(result);
}
POST with JSON (using .then() chains)
const headers = {
"Content-Type": "application/json",
};
const body = JSON.stringify({
title: "foo",
body: "bar",
userId: 1,
});
const requestOptions = {
method: "POST",
headers,
body,
};
fetch("https://jsonplaceholder.typicode.com/posts", requestOptions)
.then((response) => response.json())
.then((result) => console.log(result))
.catch((error) => console.log("error", error));
Using FormData
const body = new FormData();
body.append("title", "foo");
body.append("body", "bar");
body.append("userId", 1);
const requestOptions = {
method: "POST",
body,
};
fetch("https://jsonplaceholder.typicode.com/posts", requestOptions)
.then((response) => response.json())
.then((result) => console.log(result))
.catch((error) => console.log("error", error));
Note: When using FormData, do NOT set the
Content-Typeheader manually. The browser automatically sets it tomultipart/form-datawith the correct boundary string.
AbortController and AbortSignal
Sometimes you need to cancel in-flight requests — timeouts, user navigation, or component unmounts. AbortController provides a way to abort fetch requests and other async operations.
An instance of the AbortController class exposes the abort() method and the signal property.
Invoking abort() emits the abort event to notify the abortable API watching the controller about the cancellation.
Ordinarily, we expect the result of an asynchronous operation to succeed or fail. However, the process can also take more time than anticipated, or you may no longer need the results when you receive them.
Setting timeouts with AbortController
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);
fetch("https://jsonplaceholder.typicode.com/posts", {
signal: controller.signal,
})
.then((response) => response.json())
.then((data) => console.log(data))
.catch((error) => console.error(error))
.finally(() => clearTimeout(timeout));
Using AbortSignal.timeout()
A simpler built-in way to set a timeout:
const signal = AbortSignal.timeout(200);
const url = "https://jsonplaceholder.typicode.com/todos/1";
const fetchTodo = async () => {
try {
const response = await fetch(url, { signal });
const todo = await response.json();
console.log(todo);
} catch (error) {
if (error.name === "AbortError") {
console.log("Operation timed out");
} else {
console.error(error);
}
}
};
fetchTodo();
Using multiple abort signals — AbortSignal.any()
Combine multiple abort reasons (user cancellation + timeout) into one signal:
// Create two separate controllers for different concerns
const userController = new AbortController();
const timeoutController = new AbortController();
// Set up a timeout that will abort after 5 seconds
setTimeout(() => timeoutController.abort(), 5000);
// Register an event listener that can be aborted (removed) by either signal
document.addEventListener('click', handleUserClick, {
signal: AbortSignal.any([userController.signal, timeoutController.signal])
});