CookieStore API: The Async Future of Cookie Management in JavaScript
7 min read

CookieStore API: The Async Future of Cookie Management in JavaScript

1379 words

CookieStore API: The Async Future of Cookie Management in JavaScript

For decades, web developers have depended on the old and limited document.cookie interface to handle cookies in the browser. This synchronous API, with its peculiar string syntax, has been a source of frustration and errors. But that’s changing with the arrival of CookieStore API, a modern and asynchronous interface that promises to revolutionize how we interact with cookies.

The Problem with document.cookie

Before diving into CookieStore, let’s recall the headaches document.cookie has caused us:

Archaic and Error-Prone Syntax

// The "traditional" way - complex and error-prone
document.cookie = "username=john_doe; expires=Thu, 18 Dec 2025 12:00:00 UTC; path=/; SameSite=Strict; Secure";

// Reading cookies requires manual parsing
function getCookie(name) {
    const value = `; ${document.cookie}`;
    const parts = value.split(`; ${name}=`);
    if (parts.length === 2) return parts.pop().split(';').shift();
}

Fundamental Limitations

  • Synchronous interface: Blocks the main thread
  • Only available in window context: Doesn’t work in Service Workers
  • Manual parsing: Requires custom code to read values
  • No native events: Impossible to listen for cookie changes
  • Error-prone: Complex string syntax

Introducing CookieStore API

CookieStore API is the modern answer to these problems, offering:

  • Async interface based on Promises
  • Available in Service Workers
  • Simple and clear methods
  • ✅Native change events**
  • Better error handling
  • Typed options

Availability and Compatibility

⚠️ Important: CookieStore API is available only in secure contexts (HTTPS) and its browser support is still limited. Currently compatible with:

  • Chrome/Edge 87+
  • Opera 73+
  • Safari (experimental)
  • Firefox (in development)

Main CookieStore Methods

1. cookieStore.set() - Set Cookies

// Simple syntax: name and value
await cookieStore.set('username', 'john_doe');

// Complete syntax with options
await cookieStore.set({
    name: 'sessionToken',
    value: 'abc123xyz789',
    expires: Date.now() + (24 * 60 * 60 * 1000), // 24 hours
    domain: 'example.com',
    path: '/',
    sameSite: 'strict',
    secure: true,
    partitioned: true // For third-party cookies
});

// With error handling
try {
    await cookieStore.set('preferences', JSON.stringify(userPrefs));
    console.log('Cookie set successfully');
} catch (error) {
    console.error('Error setting cookie:', error);
}
// Get cookie by name
const userCookie = await cookieStore.get('username');
console.log(userCookie);
// Output: { name: 'username', value: 'john_doe', domain: 'example.com', ... }

// Get with specific options
const sessionCookie = await cookieStore.get({
    name: 'sessionToken',
    path: '/admin'
});

// Check if exists
if (userCookie) {
    console.log(`User: ${userCookie.value}`);
} else {
    console.log('User not identified');
}

3. cookieStore.getAll() - Get Multiple Cookies

// Get all cookies
const allCookies = await cookieStore.getAll();
console.log(`Found ${allCookies.length} cookies`);

// Filter by options
const sessionCookies = await cookieStore.getAll({
    path: '/session'
});

// Process cookies
allCookies.forEach(cookie => {
    console.log(`${cookie.name}: ${cookie.value}`);
});

4. cookieStore.delete() - Delete Cookies

// Delete by name
await cookieStore.delete('username');

// Delete with specific options
await cookieStore.delete({
    name: 'sessionToken',
    path: '/admin'
});

// With confirmation
try {
    await cookieStore.delete('tempData');
    console.log('Cookie deleted successfully');
} catch (error) {
    console.error('Error deleting cookie:', error);
}

Change Events - The Revolution

One of the most powerful features of CookieStore API is the ability to listen for cookie changes:

// Listen for cookie changes
cookieStore.addEventListener('change', (event) => {
    console.log('Cookie change detected:', event);

    // Cookies that changed
    event.changed.forEach(cookie => {
        console.log(`Cookie modified: ${cookie.name} = ${cookie.value}`);
    });

    // Cookies that were deleted
    event.deleted.forEach(cookie => {
        console.log(`Cookie deleted: ${cookie.name}`);
    });
});

// Practical example: Sync user state
cookieStore.addEventListener('change', (event) => {
    const authChange = event.changed.find(c => c.name === 'authToken') ||
                      event.deleted.find(c => c.name === 'authToken');

    if (authChange) {
        // Update UI based on auth change
        updateAuthUI();
    }
});

Practical Use Cases

1. Advanced Session Management

class SessionManager {
    async createSession(userId, permissions) {
        const sessionData = {
            userId,
            permissions,
            createdAt: Date.now(),
            lastActive: Date.now()
        };

        await cookieStore.set({
            name: 'session',
            value: JSON.stringify(sessionData),
            expires: Date.now() + (8 * 60 * 60 * 1000), // 8 hours
            sameSite: 'strict',
            secure: true,
            httpOnly: false // For access from JS
        });
    }

    async getSession() {
        const sessionCookie = await cookieStore.get('session');
        return sessionCookie ? JSON.parse(sessionCookie.value) : null;
    }

    async destroySession() {
        await cookieStore.delete('session');
    }
}

2. User Preference System

class UserPreferences {
    constructor() {
        this.preferences = {};
        this.loadPreferences();
        this.setupChangeListener();
    }

    async loadPreferences() {
        const prefCookie = await cookieStore.get('userPrefs');
        this.preferences = prefCookie ?
            JSON.parse(prefCookie.value) :
            this.getDefaultPreferences();
    }

    async updatePreference(key, value) {
        this.preferences[key] = value;

        await cookieStore.set({
            name: 'userPrefs',
            value: JSON.stringify(this.preferences),
            expires: Date.now() + (365 * 24 * 60 * 60 * 1000), // 1 year
            sameSite: 'lax'
        });

        this.applyPreference(key, value);
    }

    getDefaultPreferences() {
        return {
            theme: 'auto',
            language: 'en',
            fontSize: 16,
            notifications: true
        };
    }
}
class ConsentManager {
    constructor() {
        this.consentTypes = ['necessary', 'analytics', 'marketing', 'personalization'];
        this.initializeConsent();
    }

    async initializeConsent() {
        const consent = await cookieStore.get('cookieConsent');

        if (!consent) {
            this.showConsentBanner();
        } else {
            this.applyConsent(JSON.parse(consent.value));
        }
    }

    async updateConsent(consentData) {
        await cookieStore.set({
            name: 'cookieConsent',
            value: JSON.stringify({
                ...consentData,
                timestamp: Date.now(),
                version: '1.0'
            }),
            expires: Date.now() + (365 * 24 * 60 * 60 * 1000), // 1 year
            sameSite: 'strict',
            secure: true
        });

        this.applyConsent(consentData);
        this.cleanupNonConsentedCookies(consentData);
    }
}

Use in Service Workers

One of the most significant advantages of CookieStore API is its availability in Service Workers:

// In a Service Worker
self.addEventListener('activate', async (event) => {
    // Clean obsolete cookies when activating SW
    const oldCookies = await cookieStore.getAll({
        name: 'temp_'
    });

    for (const cookie of oldCookies) {
        await cookieStore.delete(cookie.name);
    }
});

// Sync offline data
self.addEventListener('sync', async (event) => {
    if (event.tag === 'sync-user-data') {
        const userCookie = await cookieStore.get('userData');
        if (userCookie) {
            const userData = JSON.parse(userCookie.value);
            await syncUserDataToServer(userData);
        }
    }
});

Best Practices

1. Robust Error Handling

async function safeCookieOperation(operation) {
    try {
        return await operation();
    } catch (error) {
        if (error.name === 'NotAllowedError') {
            console.warn('Cookies blocked by user');
            return null;
        } else if (error.name === 'TypeError') {
            console.error('Type error in cookie operation:', error);
            return null;
        } else {
            console.error('Unexpected error:', error);
            throw error;
        }
    }
}

// Usage
const userData = await safeCookieOperation(() =>
    cookieStore.get('userData')
);

2. Data Validation

class SecureCookieManager {
    async setSecureCookie(name, value, options = {}) {
        // Validate input
        if (!this.isValidCookieName(name)) {
            throw new Error('Invalid cookie name');
        }

        if (!this.isValidCookieValue(value)) {
            throw new Error('Invalid cookie value');
        }

        // Secure configuration by default
        const secureOptions = {
            secure: true,
            sameSite: 'strict',
            ...options
        };

        await cookieStore.set(name, value, secureOptions);
    }

    isValidCookieName(name) {
        return /^[a-zA-Z0-9_-]+$/.test(name) && name.length <= 50;
    }

    isValidCookieValue(value) {
        return typeof value === 'string' && value.length <= 4096;
    }
}

Migration from document.cookie

Gradual Migration Strategy

class CookieAdapter {
    constructor() {
        this.supportsCookieStore = 'cookieStore' in window;
    }

    async get(name) {
        if (this.supportsCookieStore) {
            return await cookieStore.get(name);
        } else {
            // Fallback to document.cookie
            return this.getLegacyCookie(name);
        }
    }

    async set(name, value, options = {}) {
        if (this.supportsCookieStore) {
            await cookieStore.set(name, value, options);
        } else {
            this.setLegacyCookie(name, value, options);
        }
    }

    // Legacy methods for compatibility
    getLegacyCookie(name) {
        const value = `; ${document.cookie}`;
        const parts = value.split(`; ${name}=`);
        if (parts.length === 2) {
            const cookieValue = parts.pop().split(';').shift();
            return { name, value: cookieValue };
        }
        return null;
    }

    setLegacyCookie(name, value, options) {
        let cookieString = `${name}=${value}`;

        if (options.expires) {
            cookieString += `; expires=${new Date(options.expires).toUTCString()}`;
        }
        if (options.path) {
            cookieString += `; path=${options.path}`;
        }
        if (options.secure) {
            cookieString += `; secure`;
        }
        if (options.sameSite) {
            cookieString += `; samesite=${options.sameSite}`;
        }

        document.cookie = cookieString;
    }
}

Security Considerations

1. Secure Configuration by Default

const SECURE_COOKIE_DEFAULTS = {
    secure: true,        // Only HTTPS
    sameSite: 'strict',  // CSRF protection
    httpOnly: false,     // Access from JS (adjust as needed)
    partitioned: true    // For third-party cookies
};

async function setSecureCookie(name, value, customOptions = {}) {
    const options = { ...SECURE_COOKIE_DEFAULTS, ...customOptions };
    await cookieStore.set(name, value, options);
}

2. Validation and Sanitization

class SecureDataHandler {
    static sanitizeValue(value) {
        // Remove dangerous characters
        return value.replace(/[<>\"'&]/g, '');
    }

    static encryptSensitiveData(data, key) {
        // Implement encryption for sensitive data
        // Use Web Crypto API for real encryption
        return btoa(JSON.stringify(data));
    }

    static async setEncryptedCookie(name, sensitiveData, key) {
        const encrypted = this.encryptSensitiveData(sensitiveData, key);
        await cookieStore.set(name, encrypted, SECURE_COOKIE_DEFAULTS);
    }
}

Conclusion: Should You Adopt CookieStore API?

Clear advantages:

  • Modern and async interface
  • Better developer experience
  • Functionality in Service Workers
  • Native change events
  • Less boilerplate code

Considerations:

  • Limited browser support
  • Only available in HTTPS
  • Requires fallbacks for complete compatibility

Recommendation: Start experimenting with CookieStore API in new projects that can tolerate limited browser support, but maintain a robust fallback system until adoption is broader.

Cookie management is evolving, and CookieStore API represents the future of this fundamental functionality. It’s time to start preparing for this transition.


Useful links:

Tags: #JavaScript #CookieStore #WebAPI #Cookies #ModernWeb #ServiceWorkers #WebDevelopment

Comments

Latest Posts

8 min

1627 words

Have you ever found yourself duplicating utilities, data types, or components across different applications? If you work on multiple projects that need to share common code, you’ve likely faced the dilemma of how to manage this shared code efficiently.

Recently, while exploring different approaches to share code between applications, I stumbled upon an elegant and simple solution that many developers overlook: Yarn symlinks. This technique might be the perfect answer if you’re looking for a lightweight alternative to complex monorepo setups.

6 min

1149 words

Idempotency in Laravel: How to Avoid Duplicates in Your APIs with Elegance

In modern API development, one of the most critical challenges is ensuring that operations don’t execute multiple times accidentally. Imagine a user making a payment and, due to connectivity issues, clicking the “Pay” button multiple times. Without proper measures, you might process multiple payments for the same transaction. This is where idempotency comes into play.

What is Idempotency?

Idempotency is a mathematical concept applied to programming that guarantees that an operation produces the same result regardless of how many times it’s executed. In the context of APIs, it means you can make the same request multiple times without causing additional side effects.

6 min

1231 words

Are you tired of seeing imports like import Logger from "../../../utils/logger" in your Node.js projects? If you develop applications with complex folder structures, you’ve surely encountered the labyrinth of dots and slashes that relative imports can become. Fortunately, TypeScript offers an elegant solution: Path Aliases.

In this complete guide, you’ll learn to configure path aliases in Node.js projects with TypeScript, forever eliminating those confusing imports and significantly improving the readability and maintainability of your code.

7 min

1313 words

The Filament v4 Beta has officially arrived, and it’s undoubtedly the most ambitious and comprehensive update in this framework’s history. After exploring in detail all the new features, I can confidently say that this version represents a quantum leap in terms of performance, ease of use, and development capabilities.

In this comprehensive analysis, we’ll explore each of the new features in Filament v4, explaining not just what’s new, but also how these improvements can transform your workflow and your application possibilities.

4 min

728 words

The Filament team has announced exciting details about the upcoming Filament v4 Beta release, and it’s undoubtedly the most anticipated version to date. Filament v4 is the largest and most feature-packed release Filament has ever had, surpassing even the massive v3 that required over 100 minor versions.

Most Notable Features of Filament v4

Nested Resources

One of the longest-standing community requests is finally becoming reality. Nested resources allow you to operate on a Filament resource within the context of a parent resource.

11 min

2211 words

How many times have you started a Laravel project manually creating models, controllers, migrations, factories, form requests, and tests one by one? If you’re like most Laravel developers, you’ve probably wasted countless hours on these repetitive tasks that, while necessary, don’t add direct value to your application’s business logic.

Laravel Blueprint is completely changing this paradigm. This code generation tool, created by Jason McCreary (the same genius behind Laravel Shift), allows you to generate multiple Laravel components from a single, readable, and expressive YAML file. In this deep analysis, we’ll explore how Blueprint can transform your development workflow and why it’s gaining traction in the Laravel community.