It is too easy to introduce bugs into AngularJS apps by using third party libraries
I like Angular, in fact I like it a lot more than I ever expected I would. But one of the issues that nobody ever tells you about is how easy it is to introduce very very subtle bugs simply by using third party libraries. The basic problem is that Angular enforces certain constraints upon you which may or may not be respected by third party libraries.
A good example, which I'm sure must happen quite frequently, is a library that moves DOM around without telling you. My specific case with this class of bug was a pop-up dialog which I had templated inside an Angular scope in my DOM, which my dialog library (provided by KendoUI) then moved out to a different area of the DOM.
It starts off thus:
html body div [angular scope] div#dialog [dialog with ng-bindings]
and then after the dialog is shown, ends up:
html body div [angular scope] div#dialog [dialog with ng-bindings]
When the scope is first instantiated everything works as usual. The bindings inside the dialog are evaluated correctly. But when the scope is destroyed, because the dialog has been moved, it doesn't get torn down with the rest of the template and lingers in the DOM, bindings and ng-* event handlers intact.
When the scope is instantiated for a second time, the dialog and its ID already exist and is outside of any scope telling it it should update. Therefore what happens here is the dialog is still bound to the original instance of scope, which is defunct. When trying to interact with this dialog, it will behave fairly normally, except all the events it invokes will fire on a defunct scope. Your dialog, which is created by clicking items on your current screen, is actually performing actions on something from a previous screen!
There is an easy fix to this; if your scope uses dialogs, you make sure you remove them from the DOM when $scope.on('$destroy')
fires.
Debugging this class of error is extremely hard. Angular makes stepping through live code a bit difficult at the best of times, and the situation you eventually end up with is
1. $scope.variable === 'expected value'
<disconnected async code>
2. $scope.variable === 'unexpected value'
The key is to recognise that $scope.variable does not change, instead it is $scope that changes. I find it helpful to add a 'destroyed' flag to destroyed scopes so if they show up in the debugger it's pretty obvious things have gone awry.
Talk is cheap