Function works only in Safari, other browsers throw `Uncaught TypeError: X is not a function`. Where's the problem?

249 views Asked by At

I implemented a new feature to our CRM and everything works as it should on Safari (macOS), but it throws Uncaught TypeError: X is not a function on every other browser we tested it on (Chrome, Firefox, Edge). This is the piece of code that is the culprit:

if (window.changeLabel === 'undefined') {
  function changeLabel() {
    // Do something
  }
  changeLabel();
} else {
  changeLabel();
}

Why is it working only on Safari? Why is changeLabel not a function even after I check for its existence? Is this not the way to check if a function exists or not?

4

There are 4 answers

0
Ben Aston On BEST ANSWER

window.changeLabel === 'undefined' will return false regardless of browser because you are comparing a property value with the string 'undefined' and not the value undefined.

In Safari's sloppy mode the following will "leak" the function declaration outside of the block within which is was declared.

if(false) { function foo() {} }
console.log(foo) // function foo() {} in Safari, undefined in Chrome-based browsers

Chrome's sloppy mode behaves differently due to a different interpretation of the spec (the Web Compatibility Annex IIRC), meaning the function does not "leak", and remains undeclared.

[More details at the end ]

And so in Safari your code accidentally works by leaking the function declaration, making it visible outside the block.

Two things to do:

  1. Use strict mode in all production-facing code
  2. Use typeof for its intended purpose of checking for undeclared identifiers
if (typeof changeLabel !== 'function') {
  window.changeLabel = function() {
    // Do something.
  }
}
// Now you can always use changeLabel...

More details

The reason for all this faffing is that the scope of function declarations inside blocks was never really properly specified in early versions of JavaScript (it was exclusively a function-scoped language back then), and so browser vendors did their own different things. When TC-39 finally decided to standardise this behavior in ES2015, the "Don't Break The Web" requirement meant that the sloppy mode specification for this edge case ended-up being insanely complicated and counterintuitive, leading to two different implementations (only one of which is correct).

0
Cerbrus On

That's not how you would usually register a polyfill. Normally, you'd do it like this:

if (window.changeLabel === 'undefined') {
  window.changeLabel = function() {
    // Do something
  }
}

changeLabel();

Note how we don't need the else block there.

0
KooiInc On

You can probe for the existence of window.changeLabel and assign it if is does not exist in one line, like:

window.changeLabel = window.changeLabel || 
  function() { console.log(`changeLabel here`); }
changeLabel();

0
U Pavan Kumar On

try this

if (window.changeLabel === 'undefined') {
  function changeLabel() {
    // Do something
  }
  changeLabel();
} else {
  function changeLabel();{
    // Do something
  }
}