Handling localStorage errors (such as quota exceeded errors)

#javascript, #webdev
Jun 25, 2022

Let’s say you want to check if localStorage is full before inserting an item: how would you do it?
Well, there’s only one way browsers tell you if the storage is full: they throw an error (commonly referred as QuotaExceededError) when you try to store an item that doesn’t fill in localStorage. So, to handle this specific use case, you must wrap localStorage.setItem in a try & catch to detect if there’s enough space in localStorage to store the item:

(function app() {
  try {
    localStorage.setItem(keyName, keyValue);
  } catch (err) {
    // Handle the case where there wasn't enough space to store the
    // item in localStorage.
  }
})();

Although this approach works, you should bear in mind that localStorage doesn’t throw only when there’s no available space. It also throws support errors (e.g., because the localStorage API is not supported in the browser) and security errors (e.g., because the localStorage API is being restricted when browsing in private mode in some browsers).

To differentiate between such errors and the errors about quota, you can try to explicitly detect QuotaExceededError and behave accordingly:

/**
 * Determines whether an error is a QuotaExceededError.
 *
 * Browsers love throwing slightly different variations of QuotaExceededError
 * (this is especially true for old browsers/versions), so we need to check
 * different fields and values to ensure we cover every edge-case.
 *
 * @param err - The error to check
 * @return Is the error a QuotaExceededError?
 */
function isQuotaExceededError(err: unknown): boolean {
  return (
    err instanceof DOMException &&
    // everything except Firefox
    (err.code === 22 ||
      // Firefox
      err.code === 1014 ||
      // test name field too, because code might not be present
      // everything except Firefox
      err.name === "QuotaExceededError" ||
      // Firefox
      err.name === "NS_ERROR_DOM_QUOTA_REACHED")
  );
}

(function app() {
  try {
    localStorage.setItem(keyName, keyValue);
  } catch (err) {
    if (isQuotaExceededError(err)) {
      // Handle the case where there wasn't enough space to store the
      // item in localStorage.
    } else {
      // Handle the case where the localStorage API is not supported.
    }
  }
});

However, there’s an even better approach than checking the error type every time we store something in localStorage.
You see, the only case where browsers throw non-quota-related errors is when the localStorage API is not supported.
So, instead of accounting for them each time we invoke setItem, we can detect the localStorage availability (once) separately before we start using it.

The complete snippet below is a slight variation of MDN’s Feature-detecting localStorage snippet. It can be used to check the support of any API implementing the Web Storage API — so it works on both localStorage and sessionStorage

/**
 * Determines whether an error is a QuotaExceededError.
 *
 * Browsers love throwing slightly different variations of QuotaExceededError
 * (this is especially true for old browsers/versions), so we need to check
 * different fields and values to ensure we cover every edge-case.
 *
 * @param err - The error to check
 * @return Is the error a QuotaExceededError?
 */
function isQuotaExceededError(err: unknown): boolean {
  return (
    err instanceof DOMException &&
    // everything except Firefox
    (err.code === 22 ||
      // Firefox
      err.code === 1014 ||
      // test name field too, because code might not be present
      // everything except Firefox
      err.name === "QuotaExceededError" ||
      // Firefox
      err.name === "NS_ERROR_DOM_QUOTA_REACHED")
  );
}

/**
 * Determines whether a storage implementing the Web Storage API (localStorage
 * or sessionStorage) is supported.
 *
 * Browsers can make the storage not accessible in different ways, such as
 * not exposing it at all on the global object or throwing errors as soon as
 * you access/store an item.
 * To account for all these cases, we try to store a dummy item using a
 * try & catch to analyze the thrown error.
 *
 * @param webStorageType - The Web Storage API to check
 * @return Is the storage supported?
 */
function isStorageSupported(
  webStorageType: "localStorage" | "sessionStorage" = "localStorage"
): boolean {
  let storage: Storage | undefined;
  try {
    storage = window[webStorageType];
    if (!storage) {
      return false;
    }
    const x = `__storage_test__`;
    storage.setItem(x, x);
    storage.removeItem(x);
    return true;
  } catch (err) {
    // We acknowledge a QuotaExceededError only if there's something
    // already stored.
    const isValidQuotaExceededError =
      isQuotaExceededError(err) && storage.length > 0;
    return isValidQuotaExceededError;
  }
}

(function app() {
  if (!isStorageSupported("localStorage")) {
    // Handle the case where the localStorage API is not supported.
    // One thing you might wanna do, for example, is to start using a different
    // storage mechanism (in-memory, a remote db, etc.)
    return;
  }

  // You can now use setItem, knowing that if it throws, it can only mean that
  // localStorage is full.
  try {
    localStorage.setItem(keyName, keyValue);
  } catch (err) {
    // Handle the case where localStorage is full.
  }
});