Safari Private Browsing and LocalStorage QuotaExceededError

This article provides an overview of how the Safari web browser manages LocalStorage and triggers the QuotaExceededError exception during Private Browsing mode. It covers the significant shift from Safari's legacy zero-byte quota restriction to its modern in-memory storage allocation, explains why the exception occurs, and demonstrates how developers can properly catch and handle this error to prevent web applications from crashing.

Legacy Behavior: The Zero-Byte Quota (Safari 10 and Earlier)

Historically, Safari handled LocalStorage in Private Browsing mode in a way that frequently broke web applications. Instead of disabling the window.localStorage object entirely, Safari kept the object available and defined, meaning standard feature-detection checks like if (window.localStorage) would return true.

However, Safari assigned a storage quota of 0 bytes to LocalStorage during Private Browsing sessions. Reading from storage using getItem() functioned without error (simply returning null), but any attempt to write data using setItem() immediately exceeded the 0-byte limit. Consequently, Safari threw a DOMException with the name QuotaExceededError (error code 22). If a web application did not explicitly wrap write operations in a try...catch block, this unhandled exception would halt JavaScript execution and break the application.

Modern Behavior: Ephemeral In-Memory Storage (Safari 11 and Later)

Starting with iOS 11 and macOS High Sierra (Safari 11), Apple changed how Private Browsing handles LocalStorage to align better with other modern browsers.

In current versions of Safari, Private Browsing creates a temporary, in-memory data store for LocalStorage. Web applications can read and write data normally during the private session, up to the standard 5MB per-origin limit. In this modern environment, Safari only throws a QuotaExceededError if the web application genuinely attempts to store more than the 5MB limit allows. Once the user closes the Private Browsing tab or window, Safari immediately purges this in-memory storage.

Recent Privacy Protections: Partitioned Storage

In addition to ephemeral storage, modern Safari utilizes Intelligent Tracking Prevention (ITP), which partitions LocalStorage in both standard and Private Browsing modes. If a third-party iframe attempts to write to LocalStorage, that storage is isolated to the specific top-level site the user is visiting, and its quota is managed separately. Exceeding the limit within that specific partition will also trigger a QuotaExceededError.

How to Handle the Exception in Code

Because users may be on older devices, or may genuinely run out of the allotted 5MB of storage space in modern Safari, relying on a simple existence check for window.localStorage is insufficient. Developers must wrap setItem() calls in a try...catch block to handle the QuotaExceededError gracefully.

Here is the standard pattern for safely writing to LocalStorage in Safari:

function safeSetLocalStorage(key, value) {
  try {
    window.localStorage.setItem(key, value);
    return true;
  } catch (error) {
    if (
      error instanceof DOMException &&
      (error.code === 22 ||
        error.name === "QuotaExceededError" ||
        error.name === "NS_ERROR_DOM_QUOTA_REACHED")
    ) {
      // Handle the QuotaExceededError (e.g., fallback to in-memory storage)
      console.warn("LocalStorage quota exceeded or disabled in Private Mode.");
      return false;
    }
    // Rethrow if it is a different, unexpected error
    throw error;
  }
}

Implementing an In-Memory Fallback

When Safari throws a QuotaExceededError, the most effective mitigation strategy is to fall back to a temporary JavaScript object. This mimics the behavior of modern Private Browsing by holding data in the browser's RAM until the page is refreshed or closed.

const memoryStorage = {};

function setStorageData(key, value) {
  const success = safeSetLocalStorage(key, value);
  if (!success) {
    memoryStorage[key] = String(value);
  }
}

function getStorageData(key) {
  const value = window.localStorage.getItem(key);
  if (value !== null) {
    return value;
  }
  return memoryStorage[key] || null;
}

By intercepting the QuotaExceededError and redirecting data to an in-memory object, web applications can ensure uninterrupted functionality regardless of the Safari version or the user's browsing mode.