Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is localStorage sometimes null in MacOS/iOS devices right after availability check?

I am experiencing a problem, which is not very frequent but still occurs on a regular basis, in a web application that makes use of local storage.

The problem is only occurring in iOS/macOS devices, regardless of browser, and the error that is being logged is the following:

TypeError: 'null' is not an object (evaluating 'localStorage.getItem')

Here's how the code works; there is a function (isLocalStorageSupported) that checks if local storage is supported, and there is another function (getLocalData) that uses it before trying to get data from the storage.

The error is thrown after the check for local storage completes by returning true, but the next line inexplicably (?) throws when trying to get a key from local storage, as the storage variable at that stage seems to be null.

It's unclear to me how this can happen, hence my question.

// The function that checks availability
const isLocalStorageSupported = (() => {
  let localStorageSupported = null;

  return () => {
    if (localStorageSupported !== null) {
      return localStorageSupported;
    }

    try {
      localStorage.setItem('check', 'check');
      localStorage.getItem('check');
      localStorage.removeItem('check');

      localStorageSupported = true;
    } catch (error) {
      localStorageSupported = false;
      console.error('localStorage not supported');
    }

    return localStorageSupported;
  };
})();

const getLocalData = (key) => {
  if (isLocalStorageSupported()) { // This check is successful

    let data = localStorage.getItem(key); // This line throws a TypeError!

    if (!data) {
      return;
    }

    return data;
  }
};
like image 407
ProgrammerPer Avatar asked Mar 09 '21 20:03

ProgrammerPer


2 Answers

localStorage will NOT throw an error if the item doesn't exist. It will return null:

//Purpose of this is to generate a completely random key to prove my point
let randomKey = String.fromCharCode(...crypto.getRandomValues(new Uint8Array(100))).match(/[A-Z]|[a-z]|[0-9]/g).join('');

//Get the value associated with the key (there is none)
console.log(localStorage.getItem(randomKey)); //--> null

If you need to check if a browser supports localStorage, check this way:

let localStorageSupported = 'localStorage' in window;
//this is equal to !!window.localStorage or Boolean(window.localStorage)

if (!localStorageSupported) {
    alert('Get a new browser!');
}
like image 85
quicVO Avatar answered Oct 16 '22 19:10

quicVO


Technically : you are caching the result of your check, and return the cached result.

If localStorage is valid on the first call to isLocalStorageSupported(), and becomes invalid afterwards, without the page being reloaded, your function would still return true.

"invalid" could maybe be a change in the state of the built-in localStorage object, or more probably some function mesing with the global localStorage object.

Extra point, probably not relevant but worth noting :
your code snippet shows an acces to an unqualified variable named localStorage (e.g : not window.localStorage), which could be provided or handled by some code in your javascript framework.


The simplest suggestion would be to not cache the result.

To avoid clobbering a possible "check" key in localStorage, you may use a more distinctive key name (e.g : "__my_company_check__").

You can confirm whether your issue comes from this caching behavior (note: this suggestion still introduces an uncached check at each call site, it will allow you to decide if your issue comes from caching the test value) :

function uncachedCheck() { ... }
function cachedCheck() { ... }

if (uncachedCheck() !== cachedCheck()) {
    alert('state of localStorage has changed !')
    // feed extra information to Splunk ...
}

I don't have a good suggestion to fix the cached version of your check : perhaps check that localStorage is still the same object ? (I don't know how different browsers support localStorage, though)

let localStorageSupported = null;
let checkedValue = undefined;

try {
if ((localStorage === checkedValue) && (localStorageSupported !== null)) {
    return localStorageSupported;
}

checkedValue = localStorage;
...
like image 35
LeGEC Avatar answered Oct 16 '22 19:10

LeGEC