Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at cf.Objective() 2009 (Minneapolis, MN) with: Bill Shelton and Adam Haskell and Marc Esher
Ben Nadel at cf.Objective() 2009 (Minneapolis, MN) with: Bill Shelton , Adam Haskell , and Marc Esher

TypeError: Cannot Read Property "childNodes" Of Undefined In AngularJS

By Ben Nadel on

The other day at InVision, we launched some code that worked fine locally, fine in QA, fine in Staging, and fine in Production - except for a small set of users. For just a few people, the page was completely breaking with the AngularJS-initiated JavaScript error, "TypeError: Cannot Read Property 'childNodes' Of Undefined." After the customer support team dug into the tickets, they noticed a trend that most of the users were in Europe; and, that most of the users had the "CookiesOK" Google Chrome plugin installed. It turns out, this was just one of a number of Google Chrome plugins that can [potentially] disrupt the AngularJS compile and linking lifecycle.


 
 
 

 
 
 
 
 

I've tried to follow the AngularJS compile and linking code to figure out exactly what is going wrong. But, to be honest, the code is a bit too complicated for me to trace effectively. I understand, at a high level, what is going wrong; but, I cannot determine the low-level landscape of details. Ultimately, the error has to do with the fact that the DOM (Document Object Model) is being altered indirectly, by a Controller, during the compile and linking phase. The alteration of the DOM throws the internal tree-walker out of whack and we end up referencing an undefined node.

In our particular case, the problem relates to a breakdown in the separation of concerns between module types. In AngularJS, the Controller is not supposed to know anything about the DOM. And, to that extend, it probably shouldn't load any services that mutate the DOM. But, that's exactly what we were doing - we were loading a service that was injecting a 3rd-party Script element as part of its initialization.

The Controller was [indirectly] mutating the DOM, which is a big no-no in AngularJS.

On its own, this may not have been a problem - or rather, the problem may never have become symptomatic. But, for users that had certain Google Chrome plugins installed, the page would break because the plugins themselves were injecting Script tags into the HTML element of the page. The 3rd-party script tags would then getting injected before the plugin-injected tags, and that's what was breaking everything.

To get a sense of what I'm talking about, here is an isolated use-case:

  • <!doctype html>
  • <html ng-app="Demo" ng-controller="AppController">
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>
  • TypeError: Cannot Read Property "childNodes" Of Undefined In AngularJS
  • </title>
  • </head>
  • <body>
  •  
  • <h1>
  • TypeError: Cannot Read Property "childNodes" Of Undefined In AngularJS
  • </h1>
  •  
  • <div>
  •  
  • <!--
  • In order for this to demo to break, we need to have a few things in play:
  •  
  • 1. We need the AppController to be on the HTML element.
  •  
  • 2. We need the AppController to trigger the loading of a 3rd-party script
  • that gets injected into document.
  •  
  • 3. We need the body to have a directive (it doesn't matter which one) that
  • is nested inside another element (ie, it cannot be a direct child of the
  • body tag).
  •  
  • 4. We need the user to have some sort of Chrome plugin like "CookieOK" or
  • "Google Analytics Opt-out Add-on" that injects a Script into the page.
  • -->
  • <div ng-style="{}">
  • Woot, there it is.
  • </div>
  •  
  • </div>
  •  
  •  
  • <!-- Load scripts. -->
  • <script type="text/javascript" src="./angular-1.4.3.js"></script>
  • <script type="text/javascript">
  •  
  • // Create an application module for our demo.
  • var app = angular.module( "Demo", [] );
  •  
  •  
  • // --------------------------------------------------------------------------- //
  • // --------------------------------------------------------------------------- //
  •  
  •  
  • // I control the root of the application.
  • angular.module( "Demo" ).controller(
  • "AppController",
  • function( $scope, thirdPartyScript ) {
  •  
  • // Trigger the loading of a script, which will be injected into the DOM.
  • thirdPartyScript.load();
  •  
  • }
  • );
  •  
  •  
  • // --------------------------------------------------------------------------- //
  • // --------------------------------------------------------------------------- //
  •  
  •  
  • // I provide the ability to load and then interact with a 3rd-party script.
  • angular.module( "Demo" ).factory(
  • "thirdPartyScript",
  • function() {
  •  
  • // Return the public API.
  • return({
  • load: load
  • });
  •  
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  •  
  • // I load the 3rd-party script tag.
  • function load() {
  •  
  • // Inject script before first script in page.
  • // --
  • // NOTE: Code like this is often copy-pasted out of some read-me
  • // on the 3rd-party vendor documentation.
  •  
  • var script = document.createElement( "script" );
  • script.src = "//cdn.some-3rd-party-vendor.com/js/script.js";
  •  
  • var firstScript = document.getElementsByTagName( "script" )[ 0 ];
  •  
  • firstScript
  • .parentNode
  • .insertBefore( script, firstScript )
  • ;
  •  
  • }
  •  
  • }
  • );
  •  
  • </script>
  •  
  • </body>
  • </html>

If I have the "CookiesOK" or the "Google Analytics Opt-out Add-on" Google Chrome plugins installed and I try to run the above page, I get the following output:


 
 
 

 
 TypeError: Cannot read property childNodes of undefined in AngularJS application. 
 
 
 

NOTE: This will not error if the controller is in a different place; or, if we don't have a nested directive in the body tag. There is something about this combination of elements that causes the internal tree-walker to get confused. But, like I said above, I can't pinpoint the actual problem in the AngularJS source code.

To fix this, we need to pull the DOM-mutation out of the Controller lifecycle. And, the easiest way to do that is simply to wrap the DOM-mutation inside of $timeout() call:

  • <!doctype html>
  • <html ng-app="Demo" ng-controller="AppController">
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>
  • TypeError: Cannot Read Property "childNodes" Of Undefined In AngularJS
  • </title>
  • </head>
  • <body>
  •  
  • <h1>
  • TypeError: Cannot Read Property "childNodes" Of Undefined In AngularJS
  • </h1>
  •  
  • <div>
  •  
  • <div ng-style="{}">
  • Woot, there it is.
  • </div>
  •  
  • </div>
  •  
  •  
  • <!-- Load scripts. -->
  • <script type="text/javascript" src="./angular-1.4.3.js"></script>
  • <script type="text/javascript">
  •  
  • // Create an application module for our demo.
  • var app = angular.module( "Demo", [] );
  •  
  •  
  • // --------------------------------------------------------------------------- //
  • // --------------------------------------------------------------------------- //
  •  
  •  
  • // I control the root of the application.
  • angular.module( "Demo" ).controller(
  • "AppController",
  • function( $scope, thirdPartyScript ) {
  •  
  • // Trigger the loading of a script, which will be injected into the DOM.
  • thirdPartyScript.load();
  •  
  • }
  • );
  •  
  •  
  • // --------------------------------------------------------------------------- //
  • // --------------------------------------------------------------------------- //
  •  
  •  
  • // I provide the ability to load and then interact with a 3rd-party script.
  • angular.module( "Demo" ).factory(
  • "thirdPartyScript",
  • function( $timeout ) {
  •  
  • // Return the public API.
  • return({
  • load: load
  • });
  •  
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  •  
  • // I load the 3rd-party script tag.
  • function load() {
  •  
  • // Apply the script inject in the next tick of the event loop. This
  • // will give AngularJS time to safely finish its compile and linking.
  • $timeout( loadSync, 0, false );
  •  
  • }
  •  
  •  
  • // ---
  • // PRIVATE METHODS.
  • // ---
  •  
  •  
  • // I load the 3rd-party script tag.
  • function loadSync() {
  •  
  • // Inject script before first script in page.
  • // --
  • // NOTE: Code like this is often copy-pasted out of some read-me
  • // on the 3rd-party vendor documentation.
  •  
  • var script = document.createElement( "script" );
  • script.src = "//cdn.some-3rd-party-vendor.com/js/script.js";
  •  
  • var firstScript = document.getElementsByTagName( "script" )[ 0 ];
  •  
  • firstScript
  • .parentNode
  • .insertBefore( script, firstScript )
  • ;
  •  
  • }
  •  
  • }
  • );
  •  
  • </script>
  •  
  • </body>
  • </html>

With this slight modification, the page runs fine, no "childNodes" error.

There's probably a better way to organize this code. But, I haven't personally dealt with loading many 3rd-party script tags; so, I don't have a good instinct for this yet. Mostly, I just wanted to get this out there in case others were running into the same, seemingly unreproducible JavaScript error.




Reader Comments

Hi Ben,
very interesting bug indeed.

You should implement custom directive for these kinds of script/css lazy loading, because as you mentioned dom manipulation in controller is a BIG no no :)

I've created jsbin as a showcase:
https://jsbin.com/kazohu/edit?html,js

look particulary at myLoadScript directive.

Reply to this Comment

Interesting and not something that you would expect, that a browser plugin would mess up your web app.

However I had something quite similar some time ago: I ran an AngularJS (Ionic) app in Chrome and the app was running in "strict-DI" mode (for strict checking of Angular Dependency Injection).

Now it was breaking with an obscure error stack trace and I couldn't find out why until I found a post on Stackoverflow related to this error and mentioning the Chrome Batarang plugin.

And yes I was running that plugin. Disabled the Batarang plugin, bingo, the app was working again in Chrome.

Reply to this Comment

I wonder if you could move ng-app directive deeper into the DOM (onto body tag, perhaps) and/or move the 3rd party script tag to the end of the DOM.

Reply to this Comment

@Martin,

I agree - moving to a directive would be the best approach. The Directive knows about the DOM; and, especially if you are doing DOM mutation inside of the link-method, you are a lot safer.

Reply to this Comment

@Fergus,

In my case, we needed the controller at the root of the HTML document as it needed to be able to control the Title tag as the user was moving around the app. That said, the structure of the DOM definitely seems to have an influence over it. And, I'm not quite sure what the underlying problem really is. For example, the problem goes away if you don't have a nested directive in side of the Body, even if that directive is entirely unrelated. Why? Not sure :) I think it has something to do with the recursive nature of compiling / linking... but, I couldn't quite pin-point the problem and I put a few hours into trying to figure it out.

Reply to this Comment

@Ronaldo,

Always happy to help put anything out there that may help others with these sort of crazy problems. Hopefully other will run across this and safe themselves some time!

Reply to this Comment

Hey Ben,
I am facing this issue when using jquery plugin named bootstrap-multiselect in angular. Can you help me out to let me know why is it so. I am loading it as per their docs and it is working also but its showing error in console.
Thanks

Reply to this Comment

Fantastic post! I ran into this same symptom and was stalled all afternoon. Debugged down to an angular managed div inside a splitter and was driving the splitter with jquery inside my controller. Broke this rule for speed of prototyping as I was using a splitter directive that didn't offer the control I needed so I just modified it in the controller. Never realized the consequence (and hadn't done that before). Thanks for taking the time to post this! Saved me some serious headaches and the $timeout(function) trick is great!

Reply to this Comment

Dear Ben,
Thanks for your useful handy website, I always wanted to have a lovely website like yours, your act is really admired.

I wanted to share my point about 'TypeError: Cannot Read Property "childNodes" Of Undefined In AngularJS'
as I was using ng-view and this happened to me very much.
using this method I solved my problem.
$scope.$on('$viewContentLoaded', function() { });

Thanks again

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.