Skip to main content
Ben Nadel at Scotch On The Rock (SOTR) 2010 (London) with: Marcos Placona
Ben Nadel at Scotch On The Rock (SOTR) 2010 (London) with: Marcos Placona

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

By
Published in Comments (17)

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.

Want to use code from this post? Check out the license.

Reader Comments

3 Comments

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.

15,848 Comments

@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.

15,848 Comments

@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.

15,848 Comments

@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!

2 Comments

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

1 Comments

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!

1 Comments

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

1 Comments

I ran into a similar problem with my Angular app blowing up with Chrome extensions. The problem that I saw was that the extension was inserting custom scripts onto the page and moving my existing script tags from <head> or <body> into <html>.

When I moved my root controller from <html ng-app="app" ng-controller="AppCtrl"> to <body ng-app="app", ng-controller="AppCtrl">, everything worked.

Obviously, this solution won't work for your situation because you need to control the page title, but I bet this will solve the problem for 99% of other people.

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel