Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Commit 3365256

Browse files
committed
fix($timeout): throw when trying to cancel non-$timeout promise
Previously, calling `$timeout.cancel()` with a promise that was not generated by a call to `$timeout()` would do nothing. This could, for example, happen when calling `.then()`/`.catch()` on the returned promise, which creates a new promise, and passing that to `$timeout.cancel()`. With this commit, `$timeout.cancel()` will throw an error if called with a non-$timeout promise, thus surfacing errors that would otherwise go unnoticed. Fixes #16424 BREAKING CHNAGE: `$timeout.cancel()` will throw an error if called with a promise that was not generated by `$timeout()`. Previously, it would silently do nothing. Before: ```js var promise = $timeout(doSomething, 1000).then(doSomethingElse); $timeout.cancel(promise); // No error; timeout NOT canceled. ``` After: ```js var promise = $timeout(doSomething, 1000).then(doSomethingElse); $timeout.cancel(promise); // Throws error. ``` Correct usage: ```js var promise = $timeout(doSomething, 1000); var newPromise = promise.then(doSomethingElse); $timeout.cancel(promise); // Timeout canceled. ```
1 parent de94dd4 commit 3365256

File tree

3 files changed

+51
-8
lines changed

3 files changed

+51
-8
lines changed
+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
@ngdoc error
2+
@name $timeout:badprom
3+
@fullName Non-$timeout promise
4+
@description
5+
6+
This error occurs when calling {@link ng.$timeout#cancel $timeout.cancel()} with a promise that
7+
was not generated by the {@link ng.$timeout $timeout} service. This can, for example, happen when
8+
calling {@link ng.$q#the-promise-api then()/catch()} on the returned promise, which creates a new
9+
promise, and pass that new promise to {@link ng.$timeout#cancel $timeout.cancel()}.
10+
11+
Example of incorrect usage that leads to this error:
12+
13+
```js
14+
var promise = $timeout(doSomething, 1000).then(doSomethingElse);
15+
$timeout.cancel(promise);
16+
```
17+
18+
To fix the example above, keep a reference to the promise returned by
19+
{@link ng.$timeout $timeout()} and pass that to {@link ng.$timeout#cancel $timeout.cancel()}:
20+
21+
```js
22+
var promise = $timeout(doSomething, 1000);
23+
var newPromise = promise.then(doSomethingElse);
24+
$timeout.cancel(promise);
25+
```

src/ng/timeout.js

+19-7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
'use strict';
22

3+
var $timeoutMinErr = minErr('$timeout');
4+
35
/** @this */
46
function $TimeoutProvider() {
57
this.$get = ['$rootScope', '$browser', '$q', '$$q', '$exceptionHandler',
@@ -83,14 +85,24 @@ function $TimeoutProvider() {
8385
* canceled.
8486
*/
8587
timeout.cancel = function(promise) {
86-
if (promise && promise.$$timeoutId in deferreds) {
87-
// Timeout cancels should not report an unhandled promise.
88-
markQExceptionHandled(deferreds[promise.$$timeoutId].promise);
89-
deferreds[promise.$$timeoutId].reject('canceled');
90-
delete deferreds[promise.$$timeoutId];
91-
return $browser.defer.cancel(promise.$$timeoutId);
88+
if (!promise) return false;
89+
90+
if (!promise.hasOwnProperty('$$timeoutId')) {
91+
throw $timeoutMinErr('badprom',
92+
'`$timeout.cancel()` called with a promise that was not generated by `$timeout()`.');
9293
}
93-
return false;
94+
95+
if (!deferreds.hasOwnProperty(promise.$$timeoutId)) return false;
96+
97+
var id = promise.$$timeoutId;
98+
var deferred = deferreds[id];
99+
100+
// Timeout cancels should not report an unhandled promise.
101+
markQExceptionHandled(deferred.promise);
102+
deferred.reject('canceled');
103+
delete deferreds[id];
104+
105+
return $browser.defer.cancel(id);
94106
};
95107

96108
return timeout;

test/ng/timeoutSpec.js

+7-1
Original file line numberDiff line numberDiff line change
@@ -280,11 +280,17 @@ describe('$timeout', function() {
280280
}));
281281

282282

283-
it('should not throw a runtime exception when given an undefined promise', inject(function($timeout) {
283+
it('should not throw an error when given an undefined promise', inject(function($timeout) {
284284
expect($timeout.cancel()).toBe(false);
285285
}));
286286

287287

288+
it('should throw an error when given a non-$timeout promise', inject(function($timeout) {
289+
var promise = $timeout(noop).then(noop);
290+
expect(function() { $timeout.cancel(promise); }).toThrowMinErr('$timeout', 'badprom');
291+
}));
292+
293+
288294
it('should forget references to relevant deferred', inject(function($timeout, $browser) {
289295
// $browser.defer.cancel is only called on cancel if the deferred object is still referenced
290296
var cancelSpy = spyOn($browser.defer, 'cancel').and.callThrough();

0 commit comments

Comments
 (0)