Proposed alternative implementation for goog.async.Deferred

395 views
Skip to first unread message

bolinfest

unread,
Dec 9, 2010, 11:40:38 AM12/9/10
to Closure Library Discuss
In using goog.async.Deferred, I have been bitten by some nasty bugs
because of some of the subtle things that it does. To that end, I am
proposing an alternative implementation named goog.deferred.Deferred
whose behavior is more explicit:

http://bolinfest.com/deferred/

Please take a look and try it out. The differences between
goog.deferred.Deferred and goog.async.Deferred are summarized in the
fileoverview:

http://bolinfest.com/deferred/deferred.js

goog.deferred.Deferred also introduces a number of new instance
methods. You can look at the unit test to see how they are intended to
be used:

http://bolinfest.com/deferred/deferred_test.js

I'm curious to hear what others think of this approach. If there is
enough support for it, I would like to contribute this into the
Closure Library and have the Library use it in place of
goog.async.Deferred.

Cheers,
Michael

Garry Boyer

unread,
Dec 9, 2010, 1:02:16 PM12/9/10
to closure-lib...@googlegroups.com
What's the motivation for removing cancellers?  I'm fine if the cancelling becomes more explicit than a constructor arg, but it's still useful for things like autocomplete, where you want to cancel any pending requests if the user types another character.

Ivan Kozik

unread,
Dec 9, 2010, 1:03:29 PM12/9/10
to closure-lib...@googlegroups.com
On Thu, Dec 9, 2010 at 16:40, bolinfest <boli...@gmail.com> wrote:
> In using goog.async.Deferred, I have been bitten by some nasty bugs
> because of some of the subtle things that it does. To that end, I am
> proposing an alternative implementation named goog.deferred.Deferred
> whose behavior is more explicit:
>
> http://bolinfest.com/deferred/

1. What's the motivation behind not wrapping errors with Error?

2. Chaining is really useful. Why would you not want to do it
automatically when a callback/errback returns a Deferred?

3. Cancellers are really useful. Twisted added them after six years,
and it has simplified the design of many other components. Maybe the
goog.async.Deferred cancellation behavior is not ideal, in which case
better behavior should probably be stolen from Twisted.


Ivan

Shawn Brenneman

unread,
Dec 9, 2010, 5:56:01 PM12/9/10
to closure-lib...@googlegroups.com
Hey Mike,

I've been using goog.async.Deferred for a while now, and have submitted one or two improvements to it so far. I think it's a very useful pattern, but the Closure implementation still has room for improvement.

Here are some thoughts on the current version as well as your implementation:

1. I've been bitten by the mistaken expectation that all errback functions will be passed Error results. Calling errback() automatically promotes its argument to an Error, but after that point any function in the callback chain may throw an arbitrary type which will be passed down the chain.

My preference would be that the error chain is used so long as the result is an Error, and that the regular callback chain is used otherwise. It turns out that making this change today breaks a lot of code: people throw all sorts of things for which "instanceof Error" isn't true.

Looking at your implementation of fire_(), it doesn't look like execution can move from the callback chain to the errback chain or vice versa. This is an important part of the Deferred pattern.

2. Chaining Deferred instances is critical for managing dependent, asynchronous actions. It allows encapsulation of complex control flows, like "download this feed, then load another feed that it references, then kick off this asynchronous UI change, then notify the user. If anything fails at any point, clean up and notify the user."

3. I find the cancellation behavior, as written, basically useless. I would like to change it so that cancellation does not always trigger the error chain, since cancellation is not always an error. For example, we might want to stop downloading a feed because the user has closed the widget that requires it.

Currently, cancel() forces every errback in the chain to be called, so each one must handle the "cancelled" error. If they don't, really ugly things could happen ("ERROR: We could not download that file. Reason: you told us to stop.")

I'd like to change the behavior so that if the user provides a cancel function, the errback is not triggered by default. The user provided canceller can always trigger the errback explicitly if they still want that behavior, but most of the time the desired behavior is to simply stop further execution of the Deferred.


That's my take on Deferred and the improvements I'd like to see. I was planning on working on the canceller changes in the next day or two, but would love to hear from other users of the class and what improvements they'd like to see. Thanks for starting this discussion!

Shawn

On Thu, Dec 9, 2010 at 8:40 AM, bolinfest <boli...@gmail.com> wrote:

Ivan Kozik

unread,
Dec 9, 2010, 6:34:14 PM12/9/10
to closure-lib...@googlegroups.com
On Thu, Dec 9, 2010 at 22:56, Shawn Brenneman <bren...@google.com> wrote:
> 3. I find the cancellation behavior, as written, basically useless. I would
> like to change it so that cancellation does not always trigger the error
> chain, since cancellation is not always an error. For example, we might want
> to stop downloading a feed because the user has closed the widget that
> requires it.

Right, cancellation is not always an error, and your canceller can
trigger the callback chain if it wants to. The implementation of
cancel in deferred.js does this after calling the canceller:

if (!this.hasFired()) {
this.errback(new goog.async.Deferred.CancelledError(this));
}

Is triggering the callback chain in the canceller enough to do what
you want? Not calling either chain would break the guarantees that
Deferreds provide, and I think it would make it harder to compose
Deferreds. I can talk more about this, but there people far more
qualified.

I'll point to these resources for Deferred design, in case anyone is interested:

1. The Twisted source code, which uses Deferreds in hundreds of
places, including some with cancellers.

2. Twisted's twisted/internet/defer.py, especially before
http://twistedmatrix.com/trac/ticket/411 was merged (which complicated
the code slightly).

3. #twisted on irc.freenode.net, which will happily talk about
Deferred design in general.

4. http://twistedmatrix.com/trac/ticket/990 , a general discussion of
how cancellers should work. It's a bit hard to wade through.

I'm not saying everything should work like their implementation, but
they have been thinking about the problem for 8 years, so it's wise to
leverage their output.

Ivan

Ivan Kozik

unread,
Dec 9, 2010, 6:46:01 PM12/9/10
to closure-lib...@googlegroups.com
On Thu, Dec 9, 2010 at 23:34, Ivan Kozik <iv...@ludios.org> wrote:
> I'll point to these resources for Deferred design, in case anyone is interested:

Forgot the mention the paper that introduced the Deferred,
"Generalization of Deferred Execution in Python":
http://www6.uniovi.es/python/pycon/papers/deferex/

as well as the modern documentation:
http://twistedmatrix.com/documents/10.2.0/core/howto/defer.html
http://ezyang.com/twisted/defer2.html

Ivan

bolinfest

unread,
Dec 10, 2010, 11:38:58 AM12/10/10
to Closure Library Discuss
A lot of points have been made, so I probably can't address all of
them, but here goes:

(1) I don't think that Error should be considered special. Since you
can't "bind" deferred.errback to a catch block, I don't see how it
helps you do the right thing -- how are you passing an Error to the
callback when you meant to pass it to the errback? What's worse is
that there is no way to extract the data from the Error created by the
call to errback(). For example:

deferred.errback({idThatFailedToLoad: 42});
deferred.addErrback(function(obj) {
fixit(obj.idThatFailedToLoad); // This doesn't work and I can't
extract the data passed to the errback!
});

The workaround I have been using is:

var e = new Error();
e.stuffICareAbout = {idThatFailedToLoad: 42};
deferred.errback(e);
deferred.addErrback(function(e) {
fixit(e.stuffICareAbout.idThatFailedToLoad);
});

I could also create a subclass of Error and use that, though it seems
fairly ridiculous to create an entire class just to pass my one value
around. Though the easiest fix that would still work would be for
goog.async.Deferred to subclass Error with a class that takes
stuffICareAbout as a constructor parameter and then has a getter so I
can access it in the errback.

(2) I think treating a callback or errback that returns a Deferred as
special is clever enough to be dangerous. I think explicit chaining
via map() or bind() (those are defined in goog.deferred.Deferred) or
some other function would be better. Similar to how an event handler
can return false or call e.preventDefault() -- I think the former is a
subtlety that most developers are not aware of and makes it too easy
to get themselves into trouble. By comparison, e.preventDefault() is
explicit and unambiguous.

(3) Neither (1) nor (2) are publicly documented in
goog.async.Deferred, which is what I say contributes most to these
horrible surprises.

(4) I think the contract of a Deferred is stronger if there is no
cancel() method. I had not noticed that it calls all of the errbacks
(until Garry pointed it out) because that is not mentioned in the
JSDoc, either. Though Shawn, I have to disagree with you on this one
because I think the one good thing that cancel() does is call all of
the errbacks; otherwise, I think it would be too easy to get a memory
leak, no?

Maybe step 1 is just to clean up the JSDoc for goog.async.Deferred. I
think we are talking about two different types of chaining, so it
would be helpful if the contract of how chaining is meant to work were
specified better. Again, I think that thinks like map() and bind() as
I have defined them (again, I'm not committed to the names) make
things more explicit and are much simpler to explain.


On Dec 9, 6:46 pm, Ivan Kozik <i...@ludios.org> wrote:

Ivan Kozik

unread,
Dec 10, 2010, 3:11:29 PM12/10/10
to closure-lib...@googlegroups.com
On Fri, Dec 10, 2010 at 16:38, bolinfest <boli...@gmail.com> wrote:
> I could also create a subclass of Error and use that, though it seems
> fairly ridiculous to create an entire class just to pass my one value
> around. Though the easiest fix that would still work would be for
> goog.async.Deferred to subclass Error with a class that takes
> stuffICareAbout as a constructor parameter and then has a getter so I
> can access it in the errback.

I agree. An Error subclass like this would be a good thing to have in
Closure Library.

> (4) I think the contract of a Deferred is stronger if there is no
> cancel() method. I had not noticed that it calls all of the errbacks
> (until Garry pointed it out) because that is not mentioned in the
> JSDoc, either. Though Shawn, I have to disagree with you on this one
> because I think the one good thing that cancel() does is call all of
> the errbacks; otherwise, I think it would be too easy to get a memory
> leak, no?

I don't think cancel() makes the contract any weaker. Cancellers
simply allow some code to suggest "stop doing stuff if possible",
which stops useless operations (because the result is no longer
needed). The Deferred still must go through either the callback or
errback chain.

FWIW, one way to think about Deferred features is to think of the
synchronous code analog. addErrback is like try {} catch {}. addBoth
is like try {} finally {}. A canceller in synchronous code would be
"if someone_stopped_me_early: abort()".


Ivan

Nathan Naze

unread,
May 9, 2011, 2:10:13 AM5/9/11
to closure-lib...@googlegroups.com
Reviving this thread. I spent some time talking with Bolin about this
last week, and also spent time researching the topic this evening. I
won't claim that deferreds/futures/promises are an area of expertise.

The current implementation is a branched and modified version of
Dojo's [1], which was based on MochiKit [2], which in turn was
inspired by Twisted [3]. These also informed the versions in jQuery
[4].

I'm not particularly fond of the current API -- the methods and names
are not intuitive ("addErrback", "addCallback" vs "addCallbacks",
etc), and it dispenses with patterns used elsewhere in Closure. At
some level, it's reimplementing observer/eventTarget, and I wonder if
it should just be one. Also, why is it not Disposable? It's a
powerful concept, but for it to be used widely, I'd want a
well-written basic class, which this isn't. Also, I've had some
painful experiences working with this version -- swallowing of runtime
errors being the worst of them.

I agree with Bolin on the confusion or returning a callback as a
special case. It conflates the two, and I'd rather chaining be more
explicit. What would help in all of these is sample usages of the new
API. I'd also like to see better tools for working with asynchronous
actions.

Finally, I am fond of jQuery's usage of "Promises" as a subset of
Deferreds, and this is a good candidate for a subclass -- read-only
views of futures. Most things should be returning "promises" that
can't be ended by outside code.
http://en.wikipedia.org/wiki/Futures_and_promises#Read-only_views

Nathan

[1] http://dojotoolkit.org/api/1.3/dojo/Deferred
[2] http://mochi.github.com/mochikit/doc/html/MochiKit/Async.html#fn-deferred
[3] http://twistedmatrix.com/documents/10.1.0/api/twisted.internet.defer.Deferred.html
[4] http://api.jquery.com/category/deferred-object/

Fredrik Blomqvist

unread,
May 9, 2011, 12:19:32 PM5/9/11
to Closure Library Discuss
Regarding "swallowing of runtime errors".

Swallowing of runtime errors could potentially be handled using this
technique:
http://dean.edwards.name/weblog/2009/03/callbacks-vs-events/ (uses a
DOM-trick, thus unfortunately only usable in a browser environment).
Or, in some cases, by simply wrapping the callback in a setTimeout(),
assuming you don't
need to manipulate the value-chain.
Personally I call setTimeout wrapping a "listener" (addListener) and
try to use it when
I only need the result, the Deferred is still passed on.

For the record, a slight modification that reduces error hiding is
already implemented in the Closure fork of MochiKit code (similar in
Dojo) by piping the previous result if the callback returns undefined,
it was discussed here:
http://groups.google.com/group/closure-library-discuss/browse_thread/thread/bea5c42a8ae193ab/d1833bb3b7d6f9dd

// Fredrik Blomqvist
> can't be ended by outside code.http://en.wikipedia.org/wiki/Futures_and_promises#Read-only_views
>
> Nathan
>
> [1]http://dojotoolkit.org/api/1.3/dojo/Deferred
> [2]http://mochi.github.com/mochikit/doc/html/MochiKit/Async.html#fn-defe...
> [3]http://twistedmatrix.com/documents/10.1.0/api/twisted.internet.defer....
> [4]http://api.jquery.com/category/deferred-object/
>
>
>
>
>
>
>
> On Fri, Dec 10, 2010 at 12:11 PM, Ivan Kozik <i...@ludios.org> wrote:

Nathan Weizenbaum

unread,
May 9, 2011, 4:04:21 PM5/9/11
to closure-lib...@googlegroups.com
I've been using Deferred heavily recently, and I'm overall quite fond of the API. The error hiding is a problem, but I've found the rest of it to work quite well. I use it in very different circumstances from those in which I use events: a Deferred is a value that will materialize at some point in the future, whereas events are notifications that something has happened. The Deferred-specific functionality of chaining values, branching, cancelling, and sending errors through the chain has been very valuable to me.

I would be saddened if chaining deferreds became substantially more verbose than it is today. I value the fluidity of being able to say

deferred.
  addCallback(function(val) { return val.foo(); }).
  addCallback(function(val) { return val.bar(); }).
  ...

Garry Boyer

unread,
May 9, 2011, 4:06:31 PM5/9/11
to clos-discuss
On Mon, May 9, 2011 at 1:04 PM, Nathan Weizenbaum <nw...@google.com> wrote:
I've been using Deferred heavily recently, and I'm overall quite fond of the API. The error hiding is a problem, but I've found the rest of it to work quite well. I use it in very different circumstances from those in which I use events: a Deferred is a value that will materialize at some point in the future, whereas events are notifications that something has happened. The Deferred-specific functionality of chaining values, branching, cancelling, and sending errors through the chain has been very valuable to me.

I would be saddened if chaining deferreds became substantially more verbose than it is today. I value the fluidity of being able to say

deferred.
  addCallback(function(val) { return val.foo(); }).
  addCallback(function(val) { return val.bar(); }).
  ...

I wouldn't mind if it was called something like .process() or similar.

Nathan Naze

unread,
May 9, 2011, 4:16:07 PM5/9/11
to closure-lib...@googlegroups.com
On Mon, May 9, 2011 at 1:04 PM, Nathan Weizenbaum <nw...@google.com> wrote:
> I've been using Deferred heavily recently, and I'm overall quite fond of the
> API. The error hiding is a problem, but I've found the rest of it to work
> quite well. I use it in very different circumstances from those in which I
> use events: a Deferred is a value that will materialize at some point in the
> future, whereas events are notifications that something has happened.

Right. "Materialization" of the value (or failure to materialize) is
the event in question.

I wouldn't want to impose a more-verbose usage, and it's probably
worth making shorthands for "attach a listener, pull the result, and
pass it to the next in line" because it's such a common operation.
jQuery calls these "filters" for use with "pipe()", or, as Garry
suggested "process".

Nathan

Nathan Weizenbaum

unread,
May 9, 2011, 4:34:11 PM5/9/11
to closure-lib...@googlegroups.com
I agree that there's something to be gained by bringing Deferred and events closer to one another, but I think it would be a mistake to unify them entirely. It sounds like what you're suggesting could be interpreted as generating a Deferred from an event target, which would be valuable. I think it's important that Deferreds aren't just event handlers which are piped together, though; that would do away with much of what's valuable about Deferreds as they are now, like cancellers and classes built on top of them (like goog.async.DeferredList and goog.messaging.DeferredChannel).

Michael Davidson

unread,
May 9, 2011, 4:40:37 PM5/9/11
to closure-lib...@googlegroups.com
It seems likely that some built-in version of Deferred will show up in a future JS standard. It might not be worth spending time on this before it makes it into the core language (unless you intend to influence the core language, in which case this probably isn't the right mailing list).

One example from Google's recently released Traceur is http://code.google.com/p/traceur-compiler/wiki/LanguageFeatures#Deferred_Functions

Michael

Michael Davidson

unread,
Jun 8, 2011, 12:10:28 PM6/8/11
to closure-lib...@googlegroups.com
Once again, reviving an old thread.

One thing that's nice about events is that when an object is disposed, it can remove itself from any event targets it's listening to.

Michael, your proposed implementation prevents calling callbacks on disposed objects (which is good), but the deferred still holds a reference to the disposed object until it fires (which is bad). Whatever the eventual solution is, I'd like to be able to have a disposed object remove itself from a deferred so that the deferred doesn't hold a reference to the disposed object.

Michael
Reply all
Reply to author
Forward
0 new messages