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])
});
Defence Preparation
Be prepared to explain topics like these:
- What is the difference between
localStorage,sessionStorage, and cookies? When would you use each? —localStoragepersists data with no expiration until explicitly cleared, scoped to the origin.sessionStoragepersists only for the duration of the browser tab/session. Cookies are sent with every HTTP request to the server and can have expiration dates. UselocalStoragefor client-side preferences or cached data,sessionStoragefor temporary per-tab state, and cookies for data that the server needs to see (like session IDs or auth tokens withHttpOnly). - What do the
HttpOnly,Secure, andSameSitecookie attributes do? Why are they important for security? —HttpOnlyprevents JavaScript from reading the cookie viadocument.cookie, protecting against XSS-based token theft.Secureensures the cookie is only sent over HTTPS.SameSitecontrols whether the cookie is sent with cross-origin requests —Strictblocks all cross-site sending,Laxallows it on top-level navigations, andNone(requiresSecure) allows it always. Together, these attributes defend against XSS and CSRF attacks. - Why does
fetch()not reject on HTTP error statuses like 404 or 500? How do you handle errors correctly? —fetch()only rejects on network failures (DNS errors, no connection). A 404 or 500 response is still a valid HTTP response from the server's perspective, so fetch resolves normally. You must checkresponse.ok(which istruefor status 200-299) orresponse.statusand throw or handle the error explicitly. This is a common gotcha compared to libraries like Axios, which reject on non-2xx statuses by default. - What is CORS, and why does the browser enforce it? — CORS (Cross-Origin Resource Sharing) is a security mechanism that prevents JavaScript on one origin (e.g.,
localhost:5173) from making requests to a different origin (e.g.,api.example.com) unless the server explicitly allows it viaAccess-Control-Allow-Originheaders. Without CORS, malicious websites could make authenticated requests to your bank API using your cookies. The browser blocks the response — the request still reaches the server, but JavaScript cannot read the result. - What does the
credentialsoption infetch()control? — Thecredentialsoption determines whether cookies and authentication headers are sent with the request.same-origin(default) sends them only to the same origin.includesends them to any origin, which is required for authenticated cross-origin API calls.omitnever sends credentials. When usinginclude, the server must also respond withAccess-Control-Allow-Credentials: trueand cannot use*forAccess-Control-Allow-Origin. - How does
AbortControllerwork for canceling fetch requests? — Create anAbortControllerinstance, pass itssignalproperty to thefetch()options, and callcontroller.abort()when you want to cancel. This rejects the fetch promise with anAbortError. It is useful for canceling requests when a user navigates away, types a new search query (cancel the old one), or when you want to enforce a timeout.AbortSignal.timeout(ms)provides a shorthand for timeout-based cancellation. - What is the difference between sending data as JSON vs
FormDatain afetchPOST request? — With JSON, you setContent-Type: application/jsonand serialize the data withJSON.stringify(). The server receives a JSON string and must parse it. WithFormData, the request is sent asmultipart/form-data— you do not set theContent-Typeheader manually (the browser adds it with the boundary).FormDatais required for file uploads and can be constructed directly from a<form>element. JSON is more common for pure data APIs. - Why is storing sensitive data like JWTs in
localStorageconsidered a security risk? —localStorageis accessible to any JavaScript running on the same origin, including scripts injected via XSS vulnerabilities. If an attacker can execute JavaScript on your page, they can read everything inlocalStorageand steal tokens.HttpOnlycookies are a safer alternative for tokens because JavaScript cannot access them at all — they are sent automatically by the browser with each request.