Skip to main content

localStorage, Cookies, Fetch, AbortController

localStorage, sessionStorage

Web storage objects localStorage and sessionStorage allow saving key/value pairs in the browser.

  • localStorage persists across browser restarts — data stays until explicitly removed.
  • sessionStorage persists 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

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 requests
    • Lax (browser default) — sent with top-level navigations but not with third-party requests
    • None — sent with all requests (requires Secure flag)
  • HttpOnly — Cookie is NOT accessible via document.cookie. Can only be set by the server via Set-Cookie header. This is the primary defense against XSS attacks stealing session cookies.

https://developer.mozilla.org/en-US/docs/Web/API/Document/cookie

Storage comparison

FeaturelocalStoragesessionStorageCookiesIndexedDB
Size limit~5-10 MB~5-10 MB~4 KBLarge (quota-based)
PersistenceUntil clearedTab sessionUntil expiryUntil cleared
Sent with requestsNoNoYes, every requestNo
APISimple key-valueSimple key-valueString-basedStructured, transactional
Best forUser preferences, cached dataTemporary tab stateAuthentication, server stateLarge 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 check response.ok or response.status yourself. 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-Type header manually. The browser automatically sets it to multipart/form-data with 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])
});