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')); + }); + }); }); });