diff --git a/docs/content/error/$interval/badprom.ngdoc b/docs/content/error/$interval/badprom.ngdoc
new file mode 100644
index 000000000000..2c9f8c5371a9
--- /dev/null
+++ b/docs/content/error/$interval/badprom.ngdoc
@@ -0,0 +1,25 @@
+@ngdoc error
+@name $interval:badprom
+@fullName Non-$interval promise
+@description
+
+This error occurs when calling {@link ng.$interval#cancel $interval.cancel()} with a promise that
+was not generated by the {@link ng.$interval $interval} service. This can, for example, happen when
+calling {@link ng.$q#the-promise-api then()/catch()} on the returned promise, which creates a new
+promise, and pass that new promise to {@link ng.$interval#cancel $interval.cancel()}.
+
+Example of incorrect usage that leads to this error:
+
+```js
+var promise = $interval(doSomething, 1000, 5).then(doSomethingElse);
+$interval.cancel(promise);
+```
+
+To fix the example above, keep a reference to the promise returned by
+{@link ng.$interval $interval()} and pass that to {@link ng.$interval#cancel $interval.cancel()}:
+
+```js
+var promise = $interval(doSomething, 1000, 5);
+var newPromise = promise.then(doSomethingElse);
+$interval.cancel(promise);
+```
diff --git a/docs/content/error/$timeout/badprom.ngdoc b/docs/content/error/$timeout/badprom.ngdoc
new file mode 100644
index 000000000000..c1b0d025ad8f
--- /dev/null
+++ b/docs/content/error/$timeout/badprom.ngdoc
@@ -0,0 +1,25 @@
+@ngdoc error
+@name $timeout:badprom
+@fullName Non-$timeout promise
+@description
+
+This error occurs when calling {@link ng.$timeout#cancel $timeout.cancel()} with a promise that
+was not generated by the {@link ng.$timeout $timeout} service. This can, for example, happen when
+calling {@link ng.$q#the-promise-api then()/catch()} on the returned promise, which creates a new
+promise, and pass that new promise to {@link ng.$timeout#cancel $timeout.cancel()}.
+
+Example of incorrect usage that leads to this error:
+
+```js
+var promise = $timeout(doSomething, 1000).then(doSomethingElse);
+$timeout.cancel(promise);
+```
+
+To fix the example above, keep a reference to the promise returned by
+{@link ng.$timeout $timeout()} and pass that to {@link ng.$timeout#cancel $timeout.cancel()}:
+
+```js
+var promise = $timeout(doSomething, 1000);
+var newPromise = promise.then(doSomethingElse);
+$timeout.cancel(promise);
+```
diff --git a/src/ng/interval.js b/src/ng/interval.js
index e5f2538f8b7b..750a6ba3df1c 100644
--- a/src/ng/interval.js
+++ b/src/ng/interval.js
@@ -1,5 +1,7 @@
'use strict';
+var $intervalMinErr = minErr('$interval');
+
/** @this */
function $IntervalProvider() {
this.$get = ['$rootScope', '$window', '$q', '$$q', '$browser',
@@ -7,132 +9,132 @@ function $IntervalProvider() {
var intervals = {};
- /**
- * @ngdoc service
- * @name $interval
- *
- * @description
- * AngularJS's wrapper for `window.setInterval`. The `fn` function is executed every `delay`
- * milliseconds.
- *
- * The return value of registering an interval function is a promise. This promise will be
- * notified upon each tick of the interval, and will be resolved after `count` iterations, or
- * run indefinitely if `count` is not defined. The value of the notification will be the
- * number of iterations that have run.
- * To cancel an interval, call `$interval.cancel(promise)`.
- *
- * In tests you can use {@link ngMock.$interval#flush `$interval.flush(millis)`} to
- * move forward by `millis` milliseconds and trigger any functions scheduled to run in that
- * time.
- *
- *
- * **Note**: Intervals created by this service must be explicitly destroyed when you are finished
- * with them. In particular they are not automatically destroyed when a controller's scope or a
- * directive's element are destroyed.
- * You should take this into consideration and make sure to always cancel the interval at the
- * appropriate moment. See the example below for more details on how and when to do this.
- *
- *
- * @param {function()} fn A function that should be called repeatedly. If no additional arguments
- * are passed (see below), the function is called with the current iteration count.
- * @param {number} delay Number of milliseconds between each function call.
- * @param {number=} [count=0] Number of times to repeat. If not set, or 0, will repeat
- * indefinitely.
- * @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise
- * will invoke `fn` within the {@link ng.$rootScope.Scope#$apply $apply} block.
- * @param {...*=} Pass additional parameters to the executed function.
- * @returns {promise} A promise which will be notified on each iteration. It will resolve once all iterations of the interval complete.
- *
- * @example
- *
- *
- *
- *
- *
- *
- *
- * Current time is:
- *
- * Blood 1 : {{blood_1}}
- * Blood 2 : {{blood_2}}
- *
- *
- *
- *
- *
- *
- *
- *
- */
+ /**
+ * @ngdoc service
+ * @name $interval
+ *
+ * @description
+ * AngularJS's wrapper for `window.setInterval`. The `fn` function is executed every `delay`
+ * milliseconds.
+ *
+ * The return value of registering an interval function is a promise. This promise will be
+ * notified upon each tick of the interval, and will be resolved after `count` iterations, or
+ * run indefinitely if `count` is not defined. The value of the notification will be the
+ * number of iterations that have run.
+ * To cancel an interval, call `$interval.cancel(promise)`.
+ *
+ * In tests you can use {@link ngMock.$interval#flush `$interval.flush(millis)`} to
+ * move forward by `millis` milliseconds and trigger any functions scheduled to run in that
+ * time.
+ *
+ *
+ * **Note**: Intervals created by this service must be explicitly destroyed when you are finished
+ * with them. In particular they are not automatically destroyed when a controller's scope or a
+ * directive's element are destroyed.
+ * You should take this into consideration and make sure to always cancel the interval at the
+ * appropriate moment. See the example below for more details on how and when to do this.
+ *
+ *
+ * @param {function()} fn A function that should be called repeatedly. If no additional arguments
+ * are passed (see below), the function is called with the current iteration count.
+ * @param {number} delay Number of milliseconds between each function call.
+ * @param {number=} [count=0] Number of times to repeat. If not set, or 0, will repeat
+ * indefinitely.
+ * @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise
+ * will invoke `fn` within the {@link ng.$rootScope.Scope#$apply $apply} block.
+ * @param {...*=} Pass additional parameters to the executed function.
+ * @returns {promise} A promise which will be notified on each iteration. It will resolve once all iterations of the interval complete.
+ *
+ * @example
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ * Current time is:
+ *
+ * Blood 1 : {{blood_1}}
+ * Blood 2 : {{blood_2}}
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ */
function interval(fn, delay, count, invokeApply) {
var hasParams = arguments.length > 4,
args = hasParams ? sliceArgs(arguments, 4) : [],
@@ -177,26 +179,36 @@ function $IntervalProvider() {
}
- /**
- * @ngdoc method
- * @name $interval#cancel
- *
- * @description
- * Cancels a task associated with the `promise`.
- *
- * @param {Promise=} promise returned by the `$interval` function.
- * @returns {boolean} Returns `true` if the task was successfully canceled.
- */
+ /**
+ * @ngdoc method
+ * @name $interval#cancel
+ *
+ * @description
+ * Cancels a task associated with the `promise`.
+ *
+ * @param {Promise=} promise returned by the `$interval` function.
+ * @returns {boolean} Returns `true` if the task was successfully canceled.
+ */
interval.cancel = function(promise) {
- if (promise && promise.$$intervalId in intervals) {
- // Interval cancels should not report as unhandled promise.
- markQExceptionHandled(intervals[promise.$$intervalId].promise);
- intervals[promise.$$intervalId].reject('canceled');
- $window.clearInterval(promise.$$intervalId);
- delete intervals[promise.$$intervalId];
- return true;
+ if (!promise) return false;
+
+ if (!promise.hasOwnProperty('$$intervalId')) {
+ throw $intervalMinErr('badprom',
+ '`$interval.cancel()` called with a promise that was not generated by `$interval()`.');
}
- return false;
+
+ if (!intervals.hasOwnProperty(promise.$$intervalId)) return false;
+
+ var id = promise.$$intervalId;
+ var deferred = intervals[id];
+
+ // Interval cancels should not report an unhandled promise.
+ markQExceptionHandled(deferred.promise);
+ deferred.reject('canceled');
+ $window.clearInterval(id);
+ delete intervals[id];
+
+ return true;
};
return interval;
diff --git a/src/ng/timeout.js b/src/ng/timeout.js
index 5c22c8b9ba55..1e4eaad3349f 100644
--- a/src/ng/timeout.js
+++ b/src/ng/timeout.js
@@ -1,5 +1,7 @@
'use strict';
+var $timeoutMinErr = minErr('$timeout');
+
/** @this */
function $TimeoutProvider() {
this.$get = ['$rootScope', '$browser', '$q', '$$q', '$exceptionHandler',
@@ -8,35 +10,35 @@ function $TimeoutProvider() {
var deferreds = {};
- /**
- * @ngdoc service
- * @name $timeout
- *
- * @description
- * AngularJS's wrapper for `window.setTimeout`. The `fn` function is wrapped into a try/catch
- * block and delegates any exceptions to
- * {@link ng.$exceptionHandler $exceptionHandler} service.
- *
- * The return value of calling `$timeout` is a promise, which will be resolved when
- * the delay has passed and the timeout function, if provided, is executed.
- *
- * To cancel a timeout request, call `$timeout.cancel(promise)`.
- *
- * In tests you can use {@link ngMock.$timeout `$timeout.flush()`} to
- * synchronously flush the queue of deferred functions.
- *
- * If you only want a promise that will be resolved after some specified delay
- * then you can call `$timeout` without the `fn` function.
- *
- * @param {function()=} fn A function, whose execution should be delayed.
- * @param {number=} [delay=0] Delay in milliseconds.
- * @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise
- * will invoke `fn` within the {@link ng.$rootScope.Scope#$apply $apply} block.
- * @param {...*=} Pass additional parameters to the executed function.
- * @returns {Promise} Promise that will be resolved when the timeout is reached. The promise
- * will be resolved with the return value of the `fn` function.
- *
- */
+ /**
+ * @ngdoc service
+ * @name $timeout
+ *
+ * @description
+ * AngularJS's wrapper for `window.setTimeout`. The `fn` function is wrapped into a try/catch
+ * block and delegates any exceptions to
+ * {@link ng.$exceptionHandler $exceptionHandler} service.
+ *
+ * The return value of calling `$timeout` is a promise, which will be resolved when
+ * the delay has passed and the timeout function, if provided, is executed.
+ *
+ * To cancel a timeout request, call `$timeout.cancel(promise)`.
+ *
+ * In tests you can use {@link ngMock.$timeout `$timeout.flush()`} to
+ * synchronously flush the queue of deferred functions.
+ *
+ * If you only want a promise that will be resolved after some specified delay
+ * then you can call `$timeout` without the `fn` function.
+ *
+ * @param {function()=} fn A function, whose execution should be delayed.
+ * @param {number=} [delay=0] Delay in milliseconds.
+ * @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise
+ * will invoke `fn` within the {@link ng.$rootScope.Scope#$apply $apply} block.
+ * @param {...*=} Pass additional parameters to the executed function.
+ * @returns {Promise} Promise that will be resolved when the timeout is reached. The promise
+ * will be resolved with the return value of the `fn` function.
+ *
+ */
function timeout(fn, delay, invokeApply) {
if (!isFunction(fn)) {
invokeApply = delay;
@@ -70,27 +72,37 @@ function $TimeoutProvider() {
}
- /**
- * @ngdoc method
- * @name $timeout#cancel
- *
- * @description
- * Cancels a task associated with the `promise`. As a result of this, the promise will be
- * resolved with a rejection.
- *
- * @param {Promise=} promise Promise returned by the `$timeout` function.
- * @returns {boolean} Returns `true` if the task hasn't executed yet and was successfully
- * canceled.
- */
+ /**
+ * @ngdoc method
+ * @name $timeout#cancel
+ *
+ * @description
+ * Cancels a task associated with the `promise`. As a result of this, the promise will be
+ * resolved with a rejection.
+ *
+ * @param {Promise=} promise Promise returned by the `$timeout` function.
+ * @returns {boolean} Returns `true` if the task hasn't executed yet and was successfully
+ * canceled.
+ */
timeout.cancel = function(promise) {
- if (promise && promise.$$timeoutId in deferreds) {
- // Timeout cancels should not report an unhandled promise.
- markQExceptionHandled(deferreds[promise.$$timeoutId].promise);
- deferreds[promise.$$timeoutId].reject('canceled');
- delete deferreds[promise.$$timeoutId];
- return $browser.defer.cancel(promise.$$timeoutId);
+ if (!promise) return false;
+
+ if (!promise.hasOwnProperty('$$timeoutId')) {
+ throw $timeoutMinErr('badprom',
+ '`$timeout.cancel()` called with a promise that was not generated by `$timeout()`.');
}
- return false;
+
+ if (!deferreds.hasOwnProperty(promise.$$timeoutId)) return false;
+
+ var id = promise.$$timeoutId;
+ var deferred = deferreds[id];
+
+ // Timeout cancels should not report an unhandled promise.
+ markQExceptionHandled(deferred.promise);
+ deferred.reject('canceled');
+ delete deferreds[id];
+
+ return $browser.defer.cancel(id);
};
return timeout;
diff --git a/test/ng/intervalSpec.js b/test/ng/intervalSpec.js
index 47281429e0b2..3b23250d1f98 100644
--- a/test/ng/intervalSpec.js
+++ b/test/ng/intervalSpec.js
@@ -335,12 +335,17 @@ describe('$interval', function() {
}));
- it('should not throw a runtime exception when given an undefined promise',
- inject(function($interval) {
+ it('should not throw an error when given an undefined promise', inject(function($interval) {
expect($interval.cancel()).toBe(false);
}));
+ it('should throw an error when given a non-$interval promise', inject(function($interval) {
+ var promise = $interval(noop).then(noop);
+ expect(function() { $interval.cancel(promise); }).toThrowMinErr('$interval', 'badprom');
+ }));
+
+
it('should not trigger digest when cancelled', inject(function($interval, $rootScope, $browser) {
var watchSpy = jasmine.createSpy('watchSpy');
$rootScope.$watch(watchSpy);
diff --git a/test/ng/timeoutSpec.js b/test/ng/timeoutSpec.js
index 648c39663c0d..bfd5d53285e7 100644
--- a/test/ng/timeoutSpec.js
+++ b/test/ng/timeoutSpec.js
@@ -280,11 +280,17 @@ describe('$timeout', function() {
}));
- it('should not throw a runtime exception when given an undefined promise', inject(function($timeout) {
+ it('should not throw an error when given an undefined promise', inject(function($timeout) {
expect($timeout.cancel()).toBe(false);
}));
+ it('should throw an error when given a non-$timeout promise', inject(function($timeout) {
+ var promise = $timeout(noop).then(noop);
+ expect(function() { $timeout.cancel(promise); }).toThrowMinErr('$timeout', 'badprom');
+ }));
+
+
it('should forget references to relevant deferred', inject(function($timeout, $browser) {
// $browser.defer.cancel is only called on cancel if the deferred object is still referenced
var cancelSpy = spyOn($browser.defer, 'cancel').and.callThrough();