The last time Hackerfall tried to access this page, it returned a not found error. A cached version of the page is below, or click here to continue anyway

iOS 8 insidious length bug breaks jQuery and Underscore object iteration

Ask anybody and they'll tell you I love Underscore. I even try teaching it when I can. Ask anybody, and they'll also tell you I don't love iPhones... Even less so today.

If you have a large JavaScript app which relies heavily on iteration functions from jQuery (like jQuery.each) or Underscore (like _.each, _.map, etc), you're in for big problem with iOS users.

Specifically, iOS 8.2 completely breaks object iteration on some devices.

So prepare to spend an entire afternoon debugging an iPhone. Unless someone saves you the trouble. Wouldn't that be wonderful?

An unexpected journey

Today we got notice that our Checkout process at VTEX was failing on the iPhone 6. We checked Analytics and iOS 8 was selling OK. Strange...

We tried some other iOS devices and could not reproduce the bug. We tried the iOS Simulator running iOS 8.2: no luck. Eventually, we got our hands on an iPhone 6 running iOS 8.2 and we could finally reproduce. Like... sometimes.

By then we knew we'd have to debug it.

Madness? This is undefined

The error itself was quite uninformative, as usual in JavaScript land (Even though the Chrome team is working hard to improve that!). Something that was supposed to have length was undefined.

TypeError: Cannot read property 'length' of undefined

We eventually tracked down the code that triggered the error, and it looked something like this:

// things = [{sellerId:'1', name: 'foo'}, {sellerId:'1', name: 'bar'}, {sellerId:'2', name: 'qux'}] 

var thingsGroupedById = _.groupBy(things, 'sellerId'); 
// { 1: [...], 2: [...] }

_.map(thingsGroupedById, function (things) {
  // things is undefined... only on iOS 8!
  for (var i = 0; i < things.length; i++) {...}
});

If you use Underscore, you'll know that mapping an object is perfectly normal. The expected result is to receive value, key on your map function. So, in this case, things should be the arrays in keys 1 and 2.

By now, we are getting pretty crazy.

If it quacks like a duck

In JavaScript, Arrays and Objects are similar, but not the same. You can access a map the same way you can access an array.

var a = [1,2,3]
a[0] // 1
var b = {0: 'foo'}
b[0] // 'foo'

However, the similarities stop there. An array has a length property which an object lacks (unless specifically defined). Therefore, many libraries use the existence of length to determine whether something is an array.

Apparently, iOS 8.2 has introduced a very creative bug. It basically makes a phantom length property available on objects with integer keys! This makes many libraries stumble and think that the object you're giving them is an array. Seriously, iOS?

(If you want the detailed explanation, have a look at the reddit discussion linked below)

Aftewards, we found that the bug has been documented on this Stack Overflow question:

A brief summary for anyone landing here from Google: There is a bug in iOS8 (possibly only on 64-bit devices) that intermittently causes a phantom "length" property to appear on objects that only have numeric properties. This causes functions such as $.each() and _.each() to incorrectly try to iterate your object as an array.

Discussion of the issue is already ongoing in both jQuery and Underscore repos.

The current theory is that this bug is the result of a failed attempt at optimization.

UPDATE: Redditor BenjaminPoulain kindly pointed out that there's already a fix for this bug on WebKit. Thanks!

Work, work, work (around)

As we couldn't replace our Underscore file at the moment, we had to be creative. Our workaround was simple enough.

var thingsGroupedById = _.groupBy(things, function(t){ return 'seller' + t.sellerId });
// { seller1: [...], seller2: [...] }

Since we never actually used the key for anything, we simply added a string prefix to stop iOS from being so smart. Gotcha!

We will probably see releases of those libraries addressing the problem soon enough as pull requests have already been proposed.

Remember: if you share this with a friend, perhaps you can save him an entire afternoon!

And as always, thanks for reading :)

UPDATE: Vlad from Softdroid kindly translated this article into Russian. Thanks!

Continue reading on firstdoit.com