AngularJS In Firefox: TypeError: Missing Argument 1 When Calling Function b.get()
Yesterday, I was up until 11PM (which is late for me) trying to track down an AngularJS error that suddenly popped up and was only affecting Firefox users. The error seemed nonsensical and manifested as a problem in the $digest loop of the AngularJS framework. Firefox kept complaining that it was missing arguments when trying to evaluate one of the $watch() expressions.
Run this demo in my JavaScript Demos project on GitHub.
The actual error message being reported was:
TypeError: Missing Argument 1 when calling function b.get()
It was being thrown in minified code, hence the "b.get()". After reproducing the problem in non-minified code, however, I was able to track the error down to the following expression in the AngularJS $digest loop:
value = watch.get(current)
After I put some break-points in the code and start logging out watch expressions, I came across one expression that was identifying itself as [native code]. This was extremely peculiar. After much hand-wringing and teeth-gnashing, I finally came across this page on the Mozilla Developer Network (MDN) that discussed a proprietary method in Firefox:
Object.prototype.watch()
At that point, I had a Eureka moment! I was able to following the stack-traces to a custom AngularJS directive that was conditionally passing "attributes.watch" to a scope.$watch() binding. When the "attributes.watch" was defined, the code worked, no problem. But, when it was not defined on the given HTML element, the code broke. The problem here was that if the "attributes.watch" wasn't defined by the code, Firefox was walking up the prototype chain (of the attributes object) and finding the "watch" property - Object.prototype.watch. This prototype method was then being passed into the scope.$watch() binding, which was throwing a fit when it was invoked with the wrong number of arguments.
Once I understood the problem, it was easy to reproduce (in all versions of AngularJS):
<!doctype html>
<html ng-app="Demo">
<head>
<meta charset="utf-8" />
<title>
AngularJS In Firefox: TypeError: Missing Argument 1 When Calling Function b.get()
</title>
</head>
<body>
<h1>
AngularJS In Firefox: TypeError: Missing Argument 1 When Calling Function b.get()
</h1>
<!--
Here, we have a simple directive - bnList - that is not going to be passed
any additional attribute data.
-->
<div bn-list>
This one has no watch.
</div>
<!--
In this version of the directive - bnList - we are going to tell it to watch
the scope expression "somethingDynamic" and then react to it. The outcome of
the change is irrelevant; the point of interest is that we have a directive
that is conditionally checking for an attribute called "watch".
-->
<div bn-list watch="somethingDynamic">
This one has a watch and is going to react to changes in "somethingDynamic."
</div>
<!-- Load scripts. -->
<script type="text/javascript" src="../../vendor/angularjs/angular-1.3.16.min.js"></script>
<script type="text/javascript">
// Create an application module for our demo.
var app = angular.module( "Demo", [] );
// --------------------------------------------------------------------------- //
// --------------------------------------------------------------------------- //
// I define a directive that conditionally reacts to some scope expression.
app.directive(
"bnList",
function() {
// Return the directive configuration object.
return({
link: link,
restrict: "A"
});
// I bind JavaScript events to the local scope.
function link( scope, element, attributes ) {
// Check to see if the "watch" attribute was passed-in. If so, we
// need to set up a watcher so that we can react to changes.
// --
// CAUTION: In Firefox, this conditional check will always be true
// because Firefox has "Object.prototype.watch()" in the root
// prototype of every object.
// --
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/watch
if ( attributes.watch ) {
// Watch for changes in the watch expression.
// --
// CAUTION: If the watch attribute actually exists, you will
// start to watch that object-local value (as you intended).
// However, if the watch attribute is not defined, this ".watch"
// reference will crawl up the prototype chain and start
// referencing the Object.prototype.watch() function described
// above. That will cause an error, no doubts.
scope.$watch( attributes.watch, handleListChanges );
}
// I update the directive state in reaction to the change in the
// watched expression.
function handleListChanges() {
// ... not relevant to this demo.
}
}
}
);
</script>
</body>
</html>
Notice that the bnList directive takes an optional "watch" attribute. On the directive instance that defines the "watch" attribute, all is good. But, on the instance that doesn't define it, we end up binding to the Firefox-native watch() method. And, when we run this code, we get the following console output:
To fix this problem, you could add a check for .hasOwnProperty() in order to ensure that Firefox won't walk up the prototype chain to look for the reference. However, it's probably best just to avoid using attributes called "watch". Or, if you do use them, make sure that they are not optional. After all, this only breaks when you have to check to see if you should be binding to "watch".
Want to use code from this post? Check out the license.
Reader Comments
Oh, yeah, a very similar one of those bit me a week or so ago. I really can't believe they want to introduce an Object.prototype method called "watch" into JS. It's just going to cause issues, since it's such a common word or variable.
@Phil,
Yeah, it does seem like a common names, especially with all the reactive frameworks that revolve around "watching" values.
Hello Ben Nadel,
can you tell me which angular versions are affected by this bug? and if there it is resolved now in a new release?
I have tried angular 1.4.0 and 1.4.5, but the bug was persistent :-(
Thanks