Skip to content
JavaScript js browser 5 min read

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.cookie never returns attributes like expires or Secure — only name=value pairs. There is no JavaScript API to inspect a cookie’s flags; that information stays inside the browser.

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.

AttributePurpose
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.
SecureOnly sent over HTTPS.
SameSite=Strict|Lax|NoneControls cross-site sending (CSRF protection).
HttpOnlyHides 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 path and domain must match when deleting. A cookie set with path=/admin cannot be removed by a delete call using the default path=/. 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.

CookieslocalStorage / sessionStorage
Sent to serverYes, on every matching requestNo, client-only
Capacity~4 KB per cookie~5–10 MB total
Expiry controlBuilt-in (expires / max-age)Manual (none for localStorage)
Server-readableYesNo
Accessible from JSUnless HttpOnlyAlways

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.

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 encodeURIComponent names and values when writing, and decodeURIComponent when reading, to survive special characters.
  • Store authentication tokens in server-set HttpOnly cookies so JavaScript (and XSS) can never read them.
  • Add Secure to any cookie carrying sensitive data so it is only sent over HTTPS.
  • Set an explicit SameSite value (Lax is a sensible default) to reduce CSRF exposure.
  • Keep cookies small — they ride along with every request, so prefer localStorage for large client-only data.
  • When deleting, reuse the exact same path and domain the cookie was created with, or the delete silently fails.
  • Prefer max-age over expires; it avoids time-zone and date-formatting bugs.
Last updated June 1, 2026
Was this helpful?