Cookies, document.cookie
A cookie is a tiny string the browser keeps on the user’s machine. Cookies are baked into the HTTP protocol itself and formally described by RFC 6265. Because they ride along with normal HTTP traffic, they became the classic way to remember who a visitor is between requests.
The usual life cycle: a server hands the browser a cookie through the Set-Cookie response header, and from then on the browser attaches that cookie to (almost) every later request back to the same domain using the Cookie request header. HTTP is stateless — each request is independent — and this handshake is what lets a server tie separate requests to the same person.
Authentication is the poster-child use case:
- When you sign in, the server replies with
Set-Cookiecarrying a unique “session identifier”. - On the next request to that domain, the browser automatically resends it via the
Cookieheader. - The server reads the identifier and knows the request came from you.
JavaScript running in the page can also reach cookies through the document.cookie property. The rest of this chapter walks through reading, writing, and the attributes that control a cookie’s reach and lifetime — there are more sharp edges here than most people expect.
Reading from document.cookie
Curious what this site has stored? On any page, the current document’s cookies are one property away:
// If the site uses an analytics tool, for example,
// there will usually be a cookie or two here.
alert( document.cookie ); // cookie1=value1; cookie2=value2;...
What you get back is a single string of name=value pairs separated by ; (semicolon and a space). Each pair is one cookie.
To pull out one cookie by name, split the string on ; and hunt for the pair you want, or match it with a regular expression. Both approaches work — you’ll write one yourself as practice, and there are ready-made helper functions near the end of this chapter.
Here’s the split-and-search version in action. The code below holds a sample cookie string exactly like the one document.cookie would give you; type a name and it walks the pairs to find the value:
Writing to document.cookie
You can assign to document.cookie, but it does not behave like a normal string property. It’s an accessor property with a getter and a setter, and the setter interprets each assignment as a command to add or update a single cookie.
Here we set a cookie named user to Maya:
document.cookie = "user=Maya"; // updates only the cookie named 'user'
alert(document.cookie); // shows ALL cookies, not just user
Run that and you’ll probably see several cookies in the output, not just user. The assignment added or replaced user and left the rest untouched.
The name and value can technically hold any characters, but the wire format uses ;, =, spaces, and commas as structure. To stay safe, run both through encodeURIComponent:
// spaces (and other special chars) need encoding
let name = "my name";
let value = "Maya Vance"
// encodes to my%20name=Maya%20Vance
document.cookie = encodeURIComponent(name) + '=' + encodeURIComponent(value);
alert(document.cookie); // ...; my%20name=Maya%20Vance
Edit either field below to watch the encoding change live — spaces become %20, and other structural characters get their own %-escapes so they can’t be mistaken for cookie syntax:
Cookies carry more than a name and value. Several attributes matter, and skipping the important ones is a common source of bugs. Attributes follow the key=value pair, each prefixed by ;:
document.cookie = "user=Maya; path=/; expires=Tue, 19 Jan 2038 03:14:07 GMT"
domain
domain=site.com
The domain attribute controls which host the cookie is visible on. You don’t get free rein here, though.
By default a cookie is visible only on the exact domain that set it, and — this catches people out — not on its subdomains. Set something at site.com and forum.site.com sees nothing:
// set on site.com...
document.cookie = "user=Maya"
// ...nothing here on forum.site.com
alert(document.cookie); // no user
You can opt into subdomain sharing. Set domain to the root domain when you write the cookie, and every subdomain gains access:
// on site.com
// make the cookie readable on any subdomain of site.com:
document.cookie = "user=Maya; domain=site.com"
// later...
// on forum.site.com
alert(document.cookie); // has user=Maya
Short version: domain is the switch that widens a cookie from a single host out to its subdomains.
path
path=/mypath
path restricts the cookie to a URL path prefix, which must be absolute. The cookie rides along only on pages at that path or below it. If you don’t set it, the browser derives a default from the current path.
Say a cookie is set with path=/admin. It’s sent for /admin and /admin/something, but not for /home, /home/admin, or the root /.
Most of the time you want the cookie everywhere on the site, so set path=/. When the attribute is omitted, the browser computes a default described in MDN’s cookie path documentation.
expires, max-age
With neither expires nor max-age, a cookie vanishes when the browser (or the tab, depending on the browser) closes. Those are session cookies.
To make a cookie outlive a browser restart, set one of these. If both are present, max-age wins.
expires=Tue, 19 Jan 2038 03:14:07 GMT
This is an absolute deletion time. Once that moment passes, the browser drops the cookie on its own. The date must use exactly this GMT-formatted layout, which is precisely what Date.prototype.toUTCString produces:
// +1 day from now
let date = new Date(Date.now() + 86400e3);
date = date.toUTCString();
document.cookie = "user=Maya; expires=" + date;
Set expires to any moment in the past and the browser deletes the cookie immediately — that’s the standard trick for removing one.
max-age=3600
An alternative to expires that takes a lifetime in seconds from right now, rather than an absolute date. Zero or a negative number deletes the cookie:
// dies in 1 hour
document.cookie = "user=Maya; max-age=3600";
// delete now (expire immediately)
document.cookie = "user=Maya; max-age=0";
gone on browser close
of deletion
≤0 deletes it
secure
secure
This flag says the cookie may only travel over HTTPS.
By default, cookies ignore the protocol entirely. A cookie set on http://site.com shows up on https://site.com too, and the reverse holds — they’re matched by domain, not by scheme.
With secure, a cookie written over https://site.com is invisible to the plain-HTTP version of the site:
// assuming we're on https:// right now
// mark the cookie HTTPS-only
document.cookie = "user=Maya; secure";
samesite
samesite is a second security-focused attribute. Its job is to blunt XSRF (cross-site request forgery) attacks. To see why it helps, look at how such an attack works.
XSRF attack
Picture yourself logged into bank.com. You hold an auth cookie from it, and the browser attaches that cookie to every request to bank.com so the server keeps recognizing you and honoring sensitive money operations.
Now, in another tab, you wander onto evil.com. That page quietly contains a form pointed at the bank — <form action="https://bank.com/pay"> — with fields that kick off a transfer to the attacker’s account, and it auto-submits.
Here’s the trap: the browser sends your bank.com cookies on any request to bank.com, even one triggered from evil.com. So the bank sees a valid, logged-in you and processes the payment.
That’s Cross-Site Request Forgery.
Real banks defend against this. Every form the bank generates includes a secret “XSRF protection token” — a value evil.com can neither guess nor read out of a cross-origin page. evil.com can fire off a form, but it can’t supply the correct token, and bank.com rejects any request missing it. (The same-origin policy is what stops evil.com from reading the token back.)
The catch is effort: you have to embed a token in every form and validate it on every incoming request. That’s a lot of surface area to get right.
Use the cookie samesite attribute
samesite offers a different line of defense, one that — in principle — removes the need for those tokens.
samesite=strict
A strict cookie is withheld any time the request originates outside the site itself.
So whether you click a link in your email, submit a form from evil.com, or trigger anything that starts on another domain, the cookie stays home. If the auth cookie is samesite=strict, the forged payment from evil.com arrives with no cookie, the bank doesn’t recognize the user, and nothing happens. The protection is solid: only actions that begin on bank.com — like submitting a form from another bank.com page — carry the cookie.
There’s one rough edge. Follow a legitimate link to bank.com from your own notes and the bank won’t recognize you on arrival, because a cross-site navigation doesn’t send a strict cookie either. That can feel broken.
A common workaround is two cookies: one loose cookie purely for “hello, Maya” recognition, and a separate samesite=strict cookie gating anything that changes data. A visitor arriving from elsewhere sees the greeting, but a payment must be started from within the bank so the strict cookie is present.
samesite=lax(also what you get fromsamesitewith no value)
lax is the gentler setting. It still blocks XSRF but avoids the “arriving from a link logs me out” surprise.
Like strict, it withholds the cookie on cross-site requests, with one carve-out. A samesite=lax cookie is sent when both conditions hold:
-
The HTTP method is “safe” — GET qualifies, POST does not.
The full roster of safe methods lives in RFC 7231. These are read-only methods that must not change data. Following a link is always a GET, so it’s safe.
-
The action is a top-level navigation — one that changes the URL in the address bar.
That’s the normal case, but navigation inside an
<iframe>isn’t top-level, and JavaScript-driven network requests don’t navigate at all, so neither qualifies.
Net effect: the everyday “click a link, go to the page” flow keeps its cookies, while the risky stuff — a cross-site fetch, a form POST from another origin — loses them.
| cross-site request | strict | lax |
|---|---|---|
| click a link (GET, top-level) | not sent | sent |
| form POST from evil.com | not sent | not sent |
| fetch/XHR from another site | not sent | not sent |
If that trade-off suits your app, samesite=lax adds real protection without wrecking the user experience.
One drawback: samesite is unknown to very old browsers (around 2017 and earlier).
httpOnly
This attribute has nothing to do with JavaScript directly, but it belongs here for completeness.
When a server sets a cookie via Set-Cookie, it can add httpOnly. That flag makes the cookie invisible to JavaScript — document.cookie can neither read it nor change it.
The point is damage control against script injection. In theory attackers should never be able to run their code on your pages, but bugs happen. If a page ends up executing an attacker’s JavaScript, that code would normally read document.cookie and walk off with the auth cookies. Mark those cookies httpOnly and document.cookie can’t see them, so injected script can’t steal them that way.
Appendix: Cookie functions
A small toolkit that’s more pleasant than poking at document.cookie by hand. Plenty of cookie libraries exist, so treat these as illustrative — though they do work.
getCookie(name)
The tersest way to grab a cookie is a regular expression. getCookie(name) returns the value, or undefined if the cookie isn’t there:
// returns the cookie with the given name,
// or undefined if not found
function getCookie(name) {
let matches = document.cookie.match(new RegExp(
"(?:^|; )" + name.replace(/([\.$?*|{}\(\)\[\]\\\/\+^])/g, '\\$1') + "=([^;]*)"
));
return matches ? decodeURIComponent(matches[1]) : undefined;
}
The new RegExp is assembled on the fly to match ; name=<value>. The name.replace(...) call escapes any regex-special characters in the name so a name like user.id is matched literally. Because values are stored encoded, getCookie runs the match back through decodeURIComponent.
setCookie(name, value, attributes)
Writes name=value, defaulting path to / (add more defaults if you like):
function setCookie(name, value, attributes = {}) {
attributes = {
path: '/',
// add other defaults here if necessary
...attributes
};
if (attributes.expires instanceof Date) {
attributes.expires = attributes.expires.toUTCString();
}
let updatedCookie = encodeURIComponent(name) + "=" + encodeURIComponent(value);
for (let attributeKey in attributes) {
updatedCookie += "; " + attributeKey;
let attributeValue = attributes[attributeKey];
if (attributeValue !== true) {
updatedCookie += "=" + attributeValue;
}
}
document.cookie = updatedCookie;
}
// Example of use:
setCookie('user', 'Maya', {secure: true, 'max-age': 3600});
Note how a boolean-flag attribute like secure: true is written as the bare word secure (the attributeValue !== true check skips the =value part), while max-age: 3600 becomes max-age=3600.
deleteCookie(name)
Deletion is just a set with an already-expired lifetime:
function deleteCookie(name) {
setCookie(name, "", {
'max-age': -1
})
}
The jar below mimics document.cookie with a plain in-memory store so you can see the whole cycle without touching the real browser cookies. Set a pair (re-using a name overwrites just that one cookie), watch the flat string rebuild, and delete individual entries:
Together, these three make a tidy little module you can drop into a project.
Appendix: Third-party cookies
A cookie is third-party when the domain that set it differs from the page you’re actually visiting.
Walk through it:
-
A page on
site.comloads a banner image from elsewhere:<img src="https://ads.com/banner.png">. -
Serving that banner,
ads.comcan attach aSet-Cookieheader — sayid=1234. That cookie belongs toads.com, and onlyads.comwill ever see it:ads.com sets its own cookie while serving a banner embedded on site.com. -
Next time
ads.comis contacted, the browser sends back theidcookie and the ad server recognizes the user:On the next banner load the browser returns the id cookie, and ads.com recognizes the same visitor. -
The powerful part: when the user moves from
site.comto a different siteother.comthat also embeds anads.combanner,ads.comreceives the same cookie again — because it’s its cookie — and can follow that person across both sites:On other.com the same ads.com banner replays the id cookie, so ads.com links the visitor across both sites.
That cross-site continuity is exactly why third-party cookies power tracking and ad networks. The cookie is glued to the domain that set it, so ads.com links the same visitor everywhere ads.com is embedded.
Plenty of people dislike being followed like this, so browsers let users turn such cookies off. Several browsers go further on their own:
- Safari blocks third-party cookies entirely.
- Firefox ships a blocklist of third-party domains whose cookies it blocks.
Appendix: GDPR
Not a JavaScript topic, but worth holding in mind when you reach for cookies.
The EU’s GDPR legislation lays down privacy rules for websites. One of them: you must get explicit consent before setting tracking cookies.
The key word is tracking. This applies only to cookies that track, identify, or authenticate a user. A cookie that merely remembers a harmless preference — and neither tracks nor identifies anyone — needs no permission. Set an auth session or a tracking ID, though, and consent is required.
In practice sites comply in one of two ways, both of which you’ve almost certainly encountered:
- Tracking cookies only for signed-up users. The registration form carries an “I accept the privacy policy” checkbox (the policy spelling out cookie usage). Once the user ticks it, auth cookies are fair game.
- Tracking cookies for everyone. The site greets newcomers with a modal “splash screen” demanding agreement before showing content. It’s intrusive — nobody enjoys a must-click wall in front of the page — but GDPR insists on explicit opt-in.
GDPR reaches well beyond cookies into other privacy matters, but those are outside our scope.
Summary
document.cookie is your window onto cookies from JavaScript.
- Each write changes only the one cookie you name.
- Encode name and value with
encodeURIComponent; decode on the way out. - Keep each cookie under 4KB, and expect a per-domain cap around 20+ (browser-dependent).
The attributes:
path=/— defaults to the current path; limits the cookie to pages under that path.domain=site.com— defaults to the setting domain only; set it explicitly to reach subdomains.expires/max-age— set a lifetime; without either, the cookie dies when the browser closes.secure— HTTPS-only.samesite— withholds the cookie on requests originating off-site, defusing XSRF.
And beyond the attributes:
- Browsers may block third-party cookies — Safari does by default, and others have followed.
- Under GDPR, tracking cookies for EU users require explicit consent.