diff --git a/src/ng/compile.js b/src/ng/compile.js
index fee065820bc4..4e906a04f821 100644
--- a/src/ng/compile.js
+++ b/src/ng/compile.js
@@ -1296,11 +1296,19 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
}
// We must run this hook in an apply since the $$postDigest runs outside apply
$rootScope.$apply(function() {
+ var errors = [];
for (var i = 0, ii = onChangesQueue.length; i < ii; ++i) {
- onChangesQueue[i]();
+ try {
+ onChangesQueue[i]();
+ } catch (e) {
+ errors.push(e);
+ }
}
// Reset the queue to trigger a new schedule next time there is a change
onChangesQueue = undefined;
+ if (errors.length) {
+ throw errors;
+ }
});
} finally {
onChangesTtl++;
@@ -2461,10 +2469,18 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
forEach(elementControllers, function(controller) {
var controllerInstance = controller.instance;
if (isFunction(controllerInstance.$onChanges)) {
- controllerInstance.$onChanges(controller.bindingInfo.initialChanges);
+ try {
+ controllerInstance.$onChanges(controller.bindingInfo.initialChanges);
+ } catch (e) {
+ $exceptionHandler(e);
+ }
}
if (isFunction(controllerInstance.$onInit)) {
- controllerInstance.$onInit();
+ try {
+ controllerInstance.$onInit();
+ } catch (e) {
+ $exceptionHandler(e);
+ }
}
if (isFunction(controllerInstance.$onDestroy)) {
controllerScope.$on('$destroy', function callOnDestroyHook() {
diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js
index 2b64f8ae8170..a9ab748b2faf 100755
--- a/test/ng/compileSpec.js
+++ b/test/ng/compileSpec.js
@@ -3563,6 +3563,46 @@ describe('$compile', function() {
expect(Controller2.prototype.$onInit).toHaveBeenCalledOnce();
});
});
+
+ it('should continue to trigger other `$onInit` hooks if one throws an error', function() {
+ function ThrowingController() {
+ this.$onInit = function() {
+ throw new Error('bad hook');
+ };
+ }
+ function LoggingController($log) {
+ this.$onInit = function() {
+ $log.info('onInit');
+ };
+ }
+
+ angular.module('my', [])
+ .component('c1', {
+ controller: ThrowingController,
+ bindings: {'prop': '<'}
+ })
+ .component('c2', {
+ controller: LoggingController,
+ bindings: {'prop': '<'}
+ })
+ .config(function($exceptionHandlerProvider) {
+ // We need to test with the exceptionHandler not rethrowing...
+ $exceptionHandlerProvider.mode('log');
+ });
+
+ module('my');
+ inject(function($compile, $rootScope, $exceptionHandler, $log) {
+
+ // Setup the directive with bindings that will keep updating the bound value forever
+ element = $compile('
')($rootScope);
+
+ // The first component's error should be logged
+ expect($exceptionHandler.errors.pop()).toEqual(new Error('bad hook'));
+
+ // The second component's hook should still be called
+ expect($log.info.logs.pop()).toEqual(['onInit']);
+ });
+ });
});
@@ -4032,6 +4072,93 @@ describe('$compile', function() {
expect($exceptionHandler.errors[0].toString()).toContain('[$compile:infchng] 10 $onChanges() iterations reached.');
});
});
+
+
+ it('should continue to trigger other `$onChanges` hooks if one throws an error', function() {
+ function ThrowingController() {
+ this.$onChanges = function(change) {
+ throw new Error('bad hook');
+ };
+ }
+ function LoggingController($log) {
+ this.$onChanges = function(change) {
+ $log.info('onChange');
+ };
+ }
+
+ angular.module('my', [])
+ .component('c1', {
+ controller: ThrowingController,
+ bindings: {'prop': '<'}
+ })
+ .component('c2', {
+ controller: LoggingController,
+ bindings: {'prop': '<'}
+ })
+ .config(function($exceptionHandlerProvider) {
+ // We need to test with the exceptionHandler not rethrowing...
+ $exceptionHandlerProvider.mode('log');
+ });
+
+ module('my');
+ inject(function($compile, $rootScope, $exceptionHandler, $log) {
+
+ // Setup the directive with bindings that will keep updating the bound value forever
+ element = $compile('
')($rootScope);
+
+ // The first component's error should be logged
+ expect($exceptionHandler.errors.pop()).toEqual(new Error('bad hook'));
+
+ // The second component's changes should still be called
+ expect($log.info.logs.pop()).toEqual(['onChange']);
+
+ $rootScope.$apply('a = 42');
+
+ // The first component's error should be logged
+ var errors = $exceptionHandler.errors.pop();
+ expect(errors[0]).toEqual(new Error('bad hook'));
+
+ // The second component's changes should still be called
+ expect($log.info.logs.pop()).toEqual(['onChange']);
+ });
+ });
+
+
+ it('should collect up all `$onChanges` errors into one throw', function() {
+ function ThrowingController() {
+ this.$onChanges = function(change) {
+ throw new Error('bad hook: ' + this.prop);
+ };
+ }
+
+ angular.module('my', [])
+ .component('c1', {
+ controller: ThrowingController,
+ bindings: {'prop': '<'}
+ })
+ .config(function($exceptionHandlerProvider) {
+ // We need to test with the exceptionHandler not rethrowing...
+ $exceptionHandlerProvider.mode('log');
+ });
+
+ module('my');
+ inject(function($compile, $rootScope, $exceptionHandler, $log) {
+
+ // Setup the directive with bindings that will keep updating the bound value forever
+ element = $compile('
')($rootScope);
+
+ // Both component's errors should be logged
+ expect($exceptionHandler.errors.pop()).toEqual(new Error('bad hook: NaN'));
+ expect($exceptionHandler.errors.pop()).toEqual(new Error('bad hook: undefined'));
+
+ $rootScope.$apply('a = 42');
+
+ // Both component's error should be logged
+ var errors = $exceptionHandler.errors.pop();
+ expect(errors.pop()).toEqual(new Error('bad hook: 84'));
+ expect(errors.pop()).toEqual(new Error('bad hook: 42'));
+ });
+ });
});
});