Strangest JavaScript Bug I've Ever Seen

June 6, 2008. Filed under javascript 18

Recently I ran into the most baffling JavaScript bug I have ever seen. In part of one method--and only in that one part of that one method--the ability to compare numbers abruptly and utterly failed.

Here is the code:

this.action = function(x,y) {
  var creeps = SET.rendering_groups[SET.creep_render_level];
  log("radius", this.radius);
  log("radius type", typeof this.radius);
  creeps.forEach(function(creep) {
    var distance = dist(x,y,creep.x,creep.y);
    log("distance", distance);
    log("distance type", typeof distance);
    log("distance <= radius", distance <= this.radius);
    log("distance < radius", distance < this.radius);
    log("radius < distance", distance > this.radius);
    log("radius <= distance", distance >= this.radius);
    if (distance < this.radius) {
      log("creep before missile", creep);
      creep.hp = Math.floor(creep.hp / 2);
      log("creep after missile", creep);
    }
  });
  play_sound("bomb");
  SET.gold -= this.cost;
};

You can see the logging statements that a friend added trying to track the problem down. Here is an output from the generated logs:

radius: 125
radius type: number
distance: 30.33081926150272
distance type: number
distance <= radius: false
distance < radius: false
radius < distance: false
radius <= distance: false

I still don't have the faintest clue what went wrong here. There is some further discussion of the bug on Processed Tower Defense's issue tracker, but suffice it to say that many work-arounds and cheap tricks were attempted, and nothing shed much light on the problem. I tried using Math.floor to do an integer comparison (thinking it might have been some kind of float bug), I tried subtracting the distance from radius and checking if the number was greater than zero... I tried a lot of things. In the end I resorted to my resort against self-inflict stupidity: I rewrote the method from scratch.

Here is the rewritten--and correctly working--code:

this.action = function(x,y) {
  var creeps = SET.rendering_groups[SET.creep_render_level];
  var l = creeps.length;
  var range = Math.floor(this.radius);
  for (var i=0;i<l;i++) {
    var creep = creeps[i];
    var d = Math.floor(dist(x,y,creep.x,creep.y));
    if (d <= range) {
      creep.hp = Math.floor(creep.hp / 2);
    }
  }
  play_sound("bomb");
  SET.gold -= this.cost;
}

The only difference I see is that I turned the forEach iteration into an explicit for loop. Could that possibly matter?