Introduction
The Cookie Store API makes all of this possible. It’s a modern, Promise-based alternative to the legacy document.cookie API that solves many of the problems developers have struggled with for years. In this article, we’ll explore Cookie Store API from the ground up. We’ll start with basics, progressively build up to advanced patterns, and create interactive demos you can play with. By the end, you’ll have all the tools you need to modernize your cookie handling.
The Problem with document.cookie
Before diving into the Cookie Store API, let’s understand why we need a new API. The traditional document.cookie approach has several limitations:
1. Synchronous and Blocking
// This blocks the main thread!
const cookies = document.cookie;
When you read document.cookie, the browser must serialize all cookies for the current domain into a single string. This is slow and blocks your JavaScript execution.
2. String Parsing is Error-Prone
// You have to manually parse this mess:
"session=abc123; user=john; theme=dark; analytics=enabled"
// Parse logic is tedious:
const cookies = document.cookie.split(';').reduce((acc, cookie) => {
const [name, value] = cookie.trim().split('=');
acc[name] = value;
return acc;
}, {});
This parsing code is duplicated across projects and prone to bugs.
3. No Service Worker Access
Service workers can’t access document.cookie because they don’t have a DOM. This makes it impossible to read cookies in:
- Offline-first apps
- Background sync scenarios
- Push notification handlers
4. No Type Safety
// Returns a string - what is this?
const value = document.cookie.split('=')[1];
// What if the cookie has an equals sign in its value?
No validation, no structure, just a string.
5. No Change Notifications
You can’t listen for cookie changes. You have to poll (inefficient) or reload the page to see updates.
The Cookie Store API solves all of these problems.
Quick Start: Your First Cookie
The Cookie Store API is accessed via the cookieStore object, available in both windows and service workers.
Basic Usage:
// Get a cookie
const cookie = await cookieStore.get('session');
console.log(cookie); // { name: 'session', value: 'abc123', ... }
// Set a cookie
await cookieStore.set('theme', 'dark');
// Delete a cookie
await cookieStore.delete('session');
That’s it! Three simple methods. But let’s try it yourself:
Notice how much easier this is than document.cookie? No parsing, no string manipulation, just clean, promise-based operations.
Cookie Properties Deep Dive
The real power of the Cookie Store API is in the options you can pass when setting cookies. Let’s explore each property:
expires vs maxAge
Two ways to control cookie lifetime:
// Using expires (absolute timestamp)
await cookieStore.set({
name: 'session',
value: 'abc123',
expires: Date.now() + 3600000 // 1 hour from now
});
// Session cookie (no expiration)
await cookieStore.set({
name: 'theme',
value: 'dark'
// No expires = session cookie, deleted when browser closes
});
domain
Controls which domains can receive the cookie:
// Only this exact domain
await cookieStore.set({
name: 'cookie',
value: 'value',
domain: 'example.com'
});
// Subdomains too (note the leading dot)
await cookieStore.set({
name: 'cookie',
value: 'value',
domain: '.example.com' // example.com, www.example.com, api.example.com
});
// Current domain only (default)
await cookieStore.set({
name: 'cookie',
value: 'value'
// No domain = only set for current domain
});
path
Restricts cookie to a specific path:
// Only accessible from /dashboard and subpaths
await cookieStore.set({
name: 'preferences',
value: 'value',
path: '/dashboard'
});
// Site-wide (default)
await cookieStore.set({
name: 'session',
value: 'value',
path: '/'
});
Secure
Only send over HTTPS connections:
await cookieStore.set({
name: 'session',
value: 'abc123',
secure: true // Only sent over HTTPS
});
Always set secure: true for authentication cookies and sensitive data.
SameSite
Controls when cookies are sent with cross-site requests:
// Strict: Only same-site requests
await cookieStore.set({
name: 'token',
value: 'abc123',
sameSite: 'Strict'
});
// Lax: Allows some cross-site (navigation)
await cookieStore.set({
name: 'token',
value: 'abc123',
sameSite: 'Lax' // Default for modern browsers
});
// None: Allows all cross-site (requires Secure)
await cookieStore.set({
name: 'token',
value: 'abc123',
sameSite: 'None',
secure: true // Required with SameSite=None
});
Partitioned
Creates a privacy-preserving partitioned cookie (more on this later):
await cookieStore.set({
name: 'analytics',
value: '123',
domain: '.tracker.com',
partitioned: true // Scoped to top-level site
});
Try configuring all these properties in the interactive builder:
Cookie Property Builder
Configure cookie properties and see the generated code in real-time.
Leave empty for current domain
Strict: only same-site • Lax: allows some cross-site • None: allows all (requires Secure)
Generated Code
await cookieStore.set({
name: 'demo_session',
value: 'user123',
path: '/',
sameSite: 'Lax',
secure: true,
})Getting Cookies
The Cookie Store API provides two methods for reading cookies:
get() - Single Cookie
// Get by name
const theme = await cookieStore.get('theme');
console.log(theme); // { name: 'theme', value: 'dark', ... }
// Get by options
const cookie = await cookieStore.get({
name: 'session',
domain: 'example.com',
path: '/api'
});
// Returns null if not found
const missing = await cookieStore.get('does-not-exist');
console.log(missing); // null
getAll() - Multiple Cookies
// Get all cookies
const allCookies = await cookieStore.getAll();
console.log(allCookies); // [{ name: 'a', value: '1' }, { name: 'b', value: '2' }]
// Filter by name
const sessionCookies = await cookieStore.getAll({ name: 'session' });
// Filter by path
const apiCookies = await cookieStore.getAll({ path: '/api' });
// Chain filters
const secureSessionCookies = await cookieStore.getAll({
name: 'session',
path: '/api',
secure: true
});
Unlike document.cookie (which returns a string), these methods return structured objects with all cookie properties.
Cookie Change Events
One of the most powerful features of the Cookie Store API is the ability to subscribe to cookie changes. This means you can react to cookies being added, modified, or deleted in real-time.
Basic Event Listener:
cookieStore.addEventListener('change', (event) => {
console.log('Cookies changed:', event);
// event.type: 'changed' | 'deleted'
// event.changed: Array of cookies that were added or modified
// event.deleted: Array of cookies that were deleted
});
Real-World Example: Auto-Logout on Token Deletion:
cookieStore.addEventListener('change', (event) => {
// Check if auth cookie was deleted
const deletedSession = event.deleted.find(c => c.name === 'session');
if (deletedSession) {
// Redirect to login page
window.location.href = '/login';
// Clear app state
logout();
}
});
Sync State Across Tabs:
cookieStore.addEventListener('change', async (event) => {
// Update theme when cookie changes
const themeCookie = event.changed.find(c => c.name === 'theme');
if (themeCookie) {
// Update UI in all tabs
document.documentElement.setAttribute('data-theme', themeCookie.value);
// Update local state
setTheme(themeCookie.value);
}
});
Try it out in the interactive demo:
Partitioned Cookies (CHIPS)
Cookies Having Independent Partitioned State (CHIPS) is a privacy-preserving feature that’s rapidly becoming essential for modern web development.
The Problem: Third-Party Cookies
Traditional third-party cookies can track users across sites:
- User visits
site-a.com→ tracker.com cookie set - User visits
site-b.com→ Same tracker.com cookie read - Tracker knows user visited both sites
This is why browsers increasingly block third-party cookies (ITP, ETP, etc.).
The Solution: Partitioned Cookies
Partitioned cookies are scoped to the top-level site:
- User visits
site-a.com→ tracker.com sets partitioned cookie - User visits
site-b.com→ Different tracker.com cookie (different partition!) - Each site gets its own tracker.com cookie → No cross-site tracking
Legitimate Use Cases:
Partitioned cookies still enable legitimate third-party functionality:
- Embedded login buttons (Google, Facebook, Twitter OAuth)
- Embedded payments (Stripe, PayPal)
- Embedded services (maps, analytics)
Each embedding site gets its own isolated cookie.
Partitioned Cookies (CHIPS)
Learn how partitioned cookies work with third-party contexts.
What Are Partitioned Cookies?
Partitioned cookies (CHIPS - Cookies Having Independent Partitioned State) are scoped to the top-level site rather than the registrable domain. This improves privacy by preventing cross-site tracking while maintaining legitimate use cases.
Traditional Cookies
- ✗Same cookie accessible from multiple sites that share a domain
- ✗Enables cross-site tracking by third-party services
- ⚠Browser restrictions (ITP) can block these cookies
Partitioned Cookies
- ✓Each top-level site gets its own partitioned cookie
- ✓Privacy-respecting alternative to traditional third-party cookies
- ✓Works with browser privacy features like ITP
When to Use Partitioned Cookies
- •Third-party authentication and session management
- •Embedded services that need state per embedding site
- •Analytics services that respect user privacy preferences
Service Worker Integration
One of the biggest advantages of the Cookie Store API is that it works in service workers. This opens up powerful new patterns:
Background Sync with Cookies:
// In service worker
self.addEventListener('sync', async (event) => {
const session = await cookieStore.get('session');
if (session) {
// Sync user data to server
await fetch('/api/sync', {
headers: {
'Authorization': `Bearer ${session.value}`
}
});
}
});
Push Notifications with Personalization:
// In service worker
self.addEventListener('push', async (event) => {
const preferences = await cookieStore.get('notifications');
if (preferences) {
// Personalize based on user preferences
const data = await event.data.json();
data.theme = JSON.parse(preferences.value).theme;
await self.registration.showNotification('Update', data);
}
});
Offline-First Authentication:
// In service worker
self.addEventListener('fetch', async (event) => {
if (event.request.url.includes('/api/')) {
const session = await cookieStore.get('session');
if (session) {
// Add auth header to all API requests
const modifiedRequest = new Request(event.request, {
headers: {
...event.request.headers,
'Authorization': `Bearer ${session.value}`
}
});
return fetch(modifiedRequest);
}
}
return event.respondWith(fetch(event.request));
});
Note: Service workers can only access cookies via
self.cookieStore, notwindow.cookieStore.
Async Patterns & Error Handling
Promise Chaining
// Chain multiple operations
await cookieStore.set('step1', 'value1');
await cookieStore.set('step2', 'value2');
await cookieStore.set('step3', 'value3');
// Parallel operations (faster!)
await Promise.all([
cookieStore.set('cookie1', 'value1'),
cookieStore.set('cookie2', 'value2'),
cookieStore.set('cookie3', 'value3')
]);
Error Handling
try {
await cookieStore.set({
name: 'session',
value: token,
secure: true,
sameSite: 'None' // ❌ Will throw without secure!
});
} catch (error) {
console.error('Failed to set cookie:', error);
// Handle error (show message to user, fallback, etc.)
}
Common errors:
- Secure context required: Only works on HTTPS (or localhost)
- Invalid SameSite:
SameSite='None'requiresSecure=true - Cookie size exceeded: Cookies limited to ~4KB
- Invalid domain: Can’t set cookies for other domains
Retry Logic
async function setCookieWithRetry(name, value, options, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
await cookieStore.set({ name, value, ...options });
return true;
} catch (error) {
if (i === maxRetries - 1) throw error;
// Exponential backoff
await new Promise(resolve => setTimeout(resolve, Math.pow(2, i) * 100));
}
}
}
Migration Guide: From document.cookie to Cookie Store API
Reading Cookies
Old way:
const cookies = document.cookie.split(';').reduce((acc, cookie) => {
const [name, value] = cookie.trim().split('=');
acc[name] = value;
return acc;
}, {});
console.log(cookies.theme);
New way:
const theme = await cookieStore.get('theme');
console.log(theme.value);
Setting Cookies
Old way:
document.cookie = 'session=abc123; Path=/; Secure; SameSite=Strict';
New way:
await cookieStore.set({
name: 'session',
value: 'abc123',
path: '/',
secure: true,
sameSite: 'Strict'
});
Deleting Cookies
Old way:
document.cookie = 'session=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT';
New way:
await cookieStore.delete('session');
Conditional Implementation
// Feature detection
if ('cookieStore' in window) {
// Use Cookie Store API
const cookie = await cookieStore.get('session');
} else {
// Fallback to document.cookie
const cookies = document.cookie;
// Parse cookie string...
}
Performance Considerations
Batch Operations
// Slow (multiple network round-trips)
for (const [name, value] of Object.entries(cookies)) {
await cookieStore.set(name, value);
}
// Fast (batched)
await Promise.all(
Object.entries(cookies).map(([name, value]) =>
cookieStore.set(name, value)
)
);
Debounce Change Handlers
let timeoutId: number;
cookieStore.addEventListener('change', () => {
clearTimeout(timeoutId);
timeoutId = window.setTimeout(() => {
// Debounced logic (e.g., re-render UI)
updateCookieUI();
}, 100);
});
Cache Frequently Accessed Cookies
const cookieCache = new Map<string, ExtendedCookieListItem>();
async function getCachedCookie(name: string) {
// Check cache first
if (cookieCache.has(name)) {
return cookieCache.get(name);
}
// Fetch from cookie store
const cookie = await cookieStore.get(name);
if (cookie) {
cookieCache.set(name, cookie);
}
return cookie;
}
// Invalidate cache on changes
cookieStore.addEventListener('change', (event) => {
event.changed.forEach(cookie => cookieCache.set(cookie.name, cookie));
event.deleted.forEach(cookie => cookieCache.delete(cookie.name));
});
Real-World Use Cases
1. Session Management
// Set session on login
async function login(credentials) {
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify(credentials)
});
const { token } = await response.json();
await cookieStore.set({
name: 'session',
value: token,
expires: Date.now() + 3600000 * 24 * 7, // 7 days
secure: true,
sameSite: 'Strict',
httpOnly: false // Can't set via JavaScript for server-only cookies
});
}
// Check session on load
async function checkAuth() {
const session = await cookieStore.get('session');
if (!session) {
window.location.href = '/login';
return false;
}
return true;
}
2. A/B Testing
async function getVariant() {
let variant = await cookieStore.get('ab_variant');
if (!variant) {
// Assign random variant
variant = Math.random() < 0.5 ? 'A' : 'B';
await cookieStore.set({
name: 'ab_variant',
value: variant,
expires: Date.now() + 3600000 * 24 * 30 // 30 days
});
}
return variant.value;
}
3. User Preferences
async function setTheme(theme: 'light' | 'dark') {
await cookieStore.set({
name: 'theme',
value: theme,
expires: Date.now() + 3600000 * 24 * 365 // 1 year
});
document.documentElement.setAttribute('data-theme', theme);
}
async function loadTheme() {
const theme = await cookieStore.get('theme');
if (theme) {
document.documentElement.setAttribute('data-theme', theme.value);
}
}
4. Cross-Domain Authentication
// Set SSO token across domains (with CORS)
async function setSSOToken(token: string, domains: string[]) {
await Promise.all(
domains.map(domain =>
fetch(`https://${domain}/api/set-token`, {
method: 'POST',
body: JSON.stringify({ token }),
credentials: 'include'
})
)
);
}
// In server endpoint (not shown):
// await cookieStore.set({ name: 'sso_token', value: token, domain });
Best Practices
1. Always Use Secure Cookies
// ❌ Bad
await cookieStore.set('session', 'token');
// ✅ Good
await cookieStore.set({
name: 'session',
value: 'token',
secure: true
});
2. Set Expiration
// ❌ Bad (session cookie when not needed)
await cookieStore.set('preferences', 'value');
// ✅ Good (explicit expiration)
await cookieStore.set({
name: 'preferences',
value: 'value',
expires: Date.now() + 3600000 * 24 * 365 // 1 year
});
3. Use SameSite Appropriately
// Authentication: Strict
await cookieStore.set({
name: 'session',
value: token,
sameSite: 'Strict',
secure: true
});
// Analytics: Lax
await cookieStore.set({
name: 'analytics_id',
value: 'abc123',
sameSite: 'Lax'
});
// Third-party: None (with Secure)
await cookieStore.set({
name: 'third_party',
value: 'value',
sameSite: 'None',
secure: true
});
4. Handle Errors Gracefully
async function setCookieSafe(options) {
try {
await cookieStore.set(options);
return true;
} catch (error) {
console.warn('Failed to set cookie:', error);
// Fallback or alternative
localStorage.setItem(options.name, options.value);
return false;
}
}
5. Use Partitioned Cookies for Third-Party Contexts
// Embedded service
await cookieStore.set({
name: 'analytics',
value: 'id123',
domain: '.analytics.com',
partitioned: true // Privacy-preserving
});
Wrapping Up
We’ve covered a lot of ground! Here’s what we explored:
- The Problem:
document.cookieis synchronous, error-prone, and unavailable in service workers - The Solution: Cookie Store API is async, Promise-based, and works everywhere
- Core Methods:
get(),getAll(),set(),delete() - Advanced Features: Change events, partitioned cookies, service worker integration
- Best Practices: Security, performance, error handling
The Cookie Store API represents a significant improvement in how we handle cookies on the web. It’s:
- Faster: Asynchronous, non-blocking operations
- Safer: Better security controls (partitioned cookies, SameSite)
- More Powerful: Works in service workers, supports change events
- Easier: No string parsing, promise-based, type-safe
Resources
The code for all interactive demos in this article is available in this repository. Feel free to explore, fork, and build your own cookie-powered experiences!
If you liked this article and think others should read it, please share it on Twitter!