Cookies
Cookies are small pieces of text the browser stores and automatically attaches to matching HTTP requests. They predate modern storage APIs and remain the backbone of session management, authentication, and personalization on the web. Reading and writing them from JavaScript happens through the deceptively simple document.cookie property, which has some genuinely surprising quirks. Understanding their attributes — expiry, scope, and security flags — is essential for building correct and secure web apps.
Reading and writing with document.cookie
The document.cookie property is both a getter and a setter, but it does not behave like a normal string property. When you read it, you get a single string of all cookies for the current document, joined by "; ". When you write to it, you set or update exactly one cookie — assignment does not overwrite the whole jar.
// Reading: returns all cookies as one string
console.log(document.cookie);
// Writing: this adds/updates ONE cookie, leaving others intact
document.cookie = "theme=dark";
document.cookie = "lang=en";
Output:
theme=dark; lang=en
Because the read format is a flat string, you must parse it yourself to retrieve a single value. Names and values should be URL-encoded so that special characters like ;, =, and spaces don’t corrupt the structure.
const setCookie = (name, value) => {
document.cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
};
const getCookie = (name) => {
const target = `${encodeURIComponent(name)}=`;
const found = document.cookie
.split("; ")
.find((row) => row.startsWith(target));
return found ? decodeURIComponent(found.slice(target.length)) : null;
};
setCookie("user", "Ada Lovelace");
console.log(getCookie("user")); // "Ada Lovelace"
Reading
document.cookienever returns attributes likeexpiresorSecure— onlyname=valuepairs. There is no JavaScript API to inspect a cookie’s flags; that information stays inside the browser.
Cookie attributes
When writing, you append attributes after the name=value pair, each separated by ; . These control how long the cookie lives, where it is sent, and how it is protected.
| Attribute | Purpose |
|---|---|
expires=<date> | Absolute expiry as a UTC string. Omit for a session cookie. |
max-age=<seconds> | Relative expiry in seconds. Takes precedence over expires. |
path=<path> | Limits the cookie to URLs under this path (default: current path). |
domain=<domain> | Shares the cookie across subdomains. |
Secure | Only sent over HTTPS. |
SameSite=Strict|Lax|None | Controls cross-site sending (CSRF protection). |
HttpOnly | Hides the cookie from JavaScript (server-set only). |
// Expires in 7 days, scoped to the whole site, HTTPS-only
const sevenDays = 60 * 60 * 24 * 7;
document.cookie =
`session=abc123; max-age=${sevenDays}; path=/; Secure; SameSite=Lax`;
A cookie without expires or max-age is a session cookie and is deleted when the browser session ends. To delete a cookie deliberately, set it again with the same name, path, and domain but an expiry in the past:
const deleteCookie = (name, path = "/") => {
document.cookie =
`${encodeURIComponent(name)}=; max-age=0; path=${path}`;
};
The
pathanddomainmust match when deleting. A cookie set withpath=/admincannot be removed by a delete call using the defaultpath=/. Mismatched scope is the most common reason “deleted” cookies stubbornly reappear.
A note on HttpOnly and security flags
HttpOnly cookies are invisible to document.cookie and can only be created by the server via the Set-Cookie response header. This is the correct place to store authentication tokens, because it shields them from theft via cross-site scripting (XSS). Secure ensures the cookie is never transmitted over plain HTTP, and SameSite mitigates cross-site request forgery (CSRF) by restricting when the browser attaches the cookie to cross-origin requests.
Cookies vs. web storage
Cookies are not a general-purpose key/value store. For data that only the client needs, localStorage and sessionStorage are simpler and roomier.
| Cookies | localStorage / sessionStorage | |
|---|---|---|
| Sent to server | Yes, on every matching request | No, client-only |
| Capacity | ~4 KB per cookie | ~5–10 MB total |
| Expiry control | Built-in (expires / max-age) | Manual (none for localStorage) |
| Server-readable | Yes | No |
| Accessible from JS | Unless HttpOnly | Always |
Reach for cookies when the server must read the value (sessions, auth, locale that affects rendering). Reach for web storage when the data is purely a client-side concern, since cookies add overhead to every request.
A reusable cookie helper
Putting the pieces together, here is a small, self-contained helper you can drop into any project.
const Cookies = {
set(name, value, { days, path = "/", secure = true, sameSite = "Lax" } = {}) {
let str = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
if (typeof days === "number") str += `; max-age=${days * 86400}`;
str += `; path=${path}; SameSite=${sameSite}`;
if (secure) str += "; Secure";
document.cookie = str;
},
get(name) {
const target = `${encodeURIComponent(name)}=`;
const row = document.cookie.split("; ").find((r) => r.startsWith(target));
return row ? decodeURIComponent(row.slice(target.length)) : null;
},
remove(name, path = "/") {
document.cookie = `${encodeURIComponent(name)}=; max-age=0; path=${path}`;
},
};
Cookies.set("greeting", "hello world", { days: 30 });
console.log(Cookies.get("greeting")); // "hello world"
Output:
hello world
Interactive demo
The demo below writes a cookie from an input, reads it back, and lets you clear it — all without a page reload.
<!DOCTYPE html>
<html>
<body style="font-family: system-ui; padding: 1rem;">
<input id="name" placeholder="Your name" />
<button id="save">Save cookie</button>
<button id="clear">Clear</button>
<p id="out">No cookie yet.</p>
<script>
const out = document.getElementById("out");
const read = () => {
const row = document.cookie
.split("; ")
.find((r) => r.startsWith("demoName="));
return row ? decodeURIComponent(row.slice("demoName=".length)) : null;
};
const render = () => {
const v = read();
out.textContent = v ? `Stored cookie: ${v}` : "No cookie yet.";
};
document.getElementById("save").addEventListener("click", () => {
const v = document.getElementById("name").value || "anonymous";
document.cookie =
`demoName=${encodeURIComponent(v)}; max-age=3600; path=/; SameSite=Lax`;
render();
});
document.getElementById("clear").addEventListener("click", () => {
document.cookie = "demoName=; max-age=0; path=/";
render();
});
render();
</script>
</body>
</html>
Best Practices
- Always
encodeURIComponentnames and values when writing, anddecodeURIComponentwhen reading, to survive special characters. - Store authentication tokens in server-set
HttpOnlycookies so JavaScript (and XSS) can never read them. - Add
Secureto any cookie carrying sensitive data so it is only sent over HTTPS. - Set an explicit
SameSitevalue (Laxis a sensible default) to reduce CSRF exposure. - Keep cookies small — they ride along with every request, so prefer
localStoragefor large client-only data. - When deleting, reuse the exact same
pathanddomainthe cookie was created with, or the delete silently fails. - Prefer
max-ageoverexpires; it avoids time-zone and date-formatting bugs.