Why is Promise.defer() deprecated and .done() discouraged?

1,430 views
Skip to first unread message

domin...@gmail.com

unread,
Jun 11, 2016, 3:17:28 PM6/11/16
to bluebird
Hello,

Could you tell me, why Promise.defer() is deprecated (link)? I understand, that it's often misused (link), but deprecating it because some people misuse it doesn't seem right to me. There are some valid cases, when using a deferred is more convenient than using Promise's constructor. Cases, when the async operation is so long, that you need to save the deferred as a local/private field, to resolve or reject it later. Any async dialog implementation is an example of it (link) - the promise is resolved/rejected outside of the Modal.open() method. If we didn't have deferred support here, we would need to construct a Promise, just to get and store the resolveFn and rejectFn for later.

My second question is: why is the usage of .done() discouraged (link: "The use of this method is heavily discouraged and it only exists for historical reasons.")? If it's discouraged, then why you mention it as one of the error handling scenarios here: link? Also, in my humble opinion, using .done() as the way to mark the end of promise chain is the ONLY way to handle errors properly in promise chains. Regardless of what my opinion is, I still don't understand why using .done() is discouraged. Unless the docs are saying, that it's discouraged to use .done() as a replacement for .then(), which I'd agree with.

Thanks.

Nate Schmolze

unread,
Jun 13, 2016, 2:59:46 PM6/13/16
to bluebird
Hey, good questions. Disclaimer: I am not involved with bluebird development; I'm just a consumer of the library. But as someone who over time has grown in his understanding of promises, I'll try to give the questions an answer; @others can correct me if I'm wrong in any point.

Could you tell me, why Promise.defer() is deprecated (link)? I understand, that it's often misused (link), but deprecating it because some people misuse it doesn't seem right to me. There are some valid cases, when using a deferred is more convenient than using Promise's constructor. Cases, when the async operation is so long, that you need to save the deferred as a local/private field, to resolve or reject it later.

I remember being supremely annoyed by this as well.  You have to write all this extra code, just to get a simple deferred object - which I totally thought I needed all the time. However, as I've worked with promises to develop my own software and libraries, being (forcibly) encouraged to avoid deferreds has significantly improved the quality and elegance of my async code.

First, a small, practical benefit.  Using the constructor is threadsafe, whereas deferreds are not:

$modal.open = function(...) {
return new Promise(resolve, reject) {
...
// exception thrown during construction is always caught && rejected
...
resolve($modal) ;
}
}
 
$modal.open = function(...) { 
var deferred = Promise.defer(); 
...
// exception thrown during construction is not caught; outer code needs to try/catch
...
deferred.resolve();
...
return $modal;
}

In the world of promises, you want to avoid having any synchronous code that isn't wrapped with a promise function. This helps to ensure that all errors are caught and thus never break the process. Deferreds encourage a development pattern that circumvents this process, whereas the constructor encourages a pattern that fosters it. As a result, deprecating defer() forces a developer to use threadsafe code, or think before s/he tries to manually create a deferred, to determine if it is really needed or not.

This leads us into the real issue -- developers' use of the library. Most of the times when I thought I needed a deferred, there was actually a better way to design my library/code.  Not always is this the case, but I would say it happened more often than not. In particular, I was used to thinking in terms of callbacks, so I was always trying to get my hands on a callback -- generally just to give it to another callback-based library.  Bluebird of course provides generic tools to help interface with callback libraries, such as promisify etc.

Promises require a paradigm shift in how you think about and design asynchronous code.  One of the policies that came out of my paradigm shift is that I almost always now return a promise from my functions, with increasingly few exceptions.

For example, you linked some code that opens a dialog box.  The modal contains a promise that represents a user response (probably true for "ok", false for "cancel", or something like that).  My expectation of a modal dialog library would be that it should return a promise. At the very least, this promise would fulfill when the dialog successfully opens (possibly to some widget controlling the modal), or preferably just to the user's choice.  This guarantees a consistent interface (a consuming developer can always rely on a function returning a promise), and it guarantees thread-safety.  The first example above represents this approach.

Long story short, deferreds work against the Promise paradigm by fostering reliance on callbacks outside the protection of a threadsafe function call.  The constructor pattern works for the Promise paradigm by making callbacks threadsafe, and discouraging their overall use, thereby encouraging the exploration of simpler and/or more "correct" solutions.

My second question is: why is the usage of .done() discouraged (link: "The use of this method is heavily discouraged and it only exists for historical reasons.")? If it's discouraged, then why you mention it as one of the error handling scenarios here: link? Also, in my humble opinion, using .done() as the way to mark the end of promise chain is the ONLY way to handle errors properly in promise chains. Regardless of what my opinion is, I still don't understand why using .done() is discouraged. 

Yeah, this is an interesting issue. Like any tool or structure in code, promises had an initial strategy of implementation, but as they've been studied in real-world environments, our understanding of how best to design/use them has changed (look for example at the differences between Promise A vs Promise A+ specifications).

The problem with .done() is kind of similar to the problem of SQL injection.  SQL libraries in any language will generally provide a way to escape/sanitize values so that they can be sent to the database securely, without corrupting the SQL language that tells the database what to do with the information. The trouble is, if a developer forgets to sanitize just one value, that opens up a hole in his security that can potentially destroy his entire database.

.done() is similar.  It is designed to crash the current process if an error is encountered that cannot be handled by the system (a lot like an unhandled exception). One objection to this would be that well-designed code should never hard-fail; it should always crash in a way that e.g. relays crash information to the appropriate parties in a reasonably professional format.

However, even if we're cool with hard-crashes in production code, .done() presents a bigger problem.  If a developer forgets to use it in just one place, an error that should crash the process will silently fail.  Errors will be happening, but the code will continue on as though nothing is wrong, which can lead to other bugs/errors/problems.  The root of these problems can very difficult to trace; for example, if data is being pushed into a queue, and popped out when a promise fulfills, an unhandled error can cause the queue to consume all available memory, crashing the process with little clue as to the reason (just that we have a memory leak).

So even though .done() sounds like a good idea, as we've watched promises operate in the real world, it is one of those design choices which many have come to believe is flawed.  Because Bluebird is designed to be a drop-in replacement for default browser library, things like .done() and .defer() have to be supported.  However there are better ways to handle their use cases, and they are therefore discouraged in new code.

In the place of .done(), Bluebird checks for unhandled rejections at the (beginning?) of each tick, which usually (not always) means that the rejection will never be handled (handlers are generally attached before promise resolution, but not always).  By default it then logs a message in the console, so that no unresolved error will go unreported.  This can be overridden with global handlers that define other functionality (like process crashing, if that's really desired). As a result, even if a developer forgets  to use .done(), the error will no longer be swallowed silently.

I hope that helps to answer your questions; they're both ones that I've had in the past.
God bless,
~ Nate

domin...@gmail.com

unread,
Jun 13, 2016, 6:39:14 PM6/13/16
to bluebird
Hello Nate,

First of all, I really appreciate, that you spent your free time writing up such an elaborate answer. Thanks.

I've got nothing to add really, you've covered it all. 

I wasn't aware of the fact, that exceptions thrown from the Promise constructor will reject that Promise. That makes sense, when I think of it now. I will pay more attention from now on to where I need deferreds and whether it'd be more appropriate to actually use a Promise constructor instead.

About the .done() part, I agree, that it requires discipline to some extend. But even after hours of coding, I've never forgotten to end a promise chain in the right place. The rule is dead simple: you either return the Promise or you .done() it. I will take a look at bluebird's alternative though, maybe it's worth giving a try. The reason I'm using .done() so far is not that I like crashing the process. I'm using it to tell the library, that at a given point there is no more code to handle the error if it reaches that point. I don't code in node.js, so "crashing a process" essentially means to me calling my window.onerror listener, where I do log the error and show a popup to the user with some advice on what just has happened and what to do next. I don't want to show that popup, if there are some more .catch() handlers in a promise chain, that might recover from the error. When I think of it though, I've never had a need of "async" .catch() handlers in my code, so it feels doable for a promise library to be able to implement an auto detection of unhandled rejections. Maybe this is what bluebird does, so I'm eager to check it out and give it a go.

One more thing: it doesn't seem to me, that you use the word "threadsafe" in the right context. Thread safety relates to handling shared data by multiple threads, as to not corrupt that data by e.g. simultaneous write from multiple threads. You seem to call a piece of code "threadsafe", when it doesn't crash a process/thread, when an exception is thrown and gets unhandled. I might've misunderstood you, though.

Ok, thanks again for your answers. I will try to challenge my current ways of thinking with these pieces of advice.

Nate Schmolze

unread,
Jun 14, 2016, 12:34:50 PM6/14/16
to bluebird
Yeah, I was really trying to find a better word than "threadsafe" when I was writing that, but I couldn't think of one, and I figured, "eh, it's close enough; nobody's gonna call me on it" :-)  Grats on precise your terminology -- a trait that I generally try to emulate :-)

JavaScript of course is single-threaded, so the word "threadsafe" should have little applicability in the language.  But it's been applied in situations where threads are simulated, such as a non-blocking io model like node (my primary area of promise use).  Basically in node, callbacks and/or promises are used to simulate multithreading capabilities.  When (e.g.) one web request comes in, it is processed until it hits a need for io (e.g. a database call).  The database call returns a promise that will fulfill when the requested data is read, and the node process continues on to handle the next web request, thus simulating multiple threads.

Because all of these "threads" actually use the same process and execution context, if .done() is used, an unhandled exception will bubble to the surface and crash the entire process, killing threads that have nothing to do with the particular error.  As a result, in a node-like context, .done() isn't "threadsafe" in the sense that an error from one thread threatens the entire collection of potential thousands.  Most thrown errors in node will crash the process (due to the way its non-blocking io model works), which is why throwing exceptions generally isn't considered threadsafe (outside of Promise contexts, which make it safe again), and thus is often discouraged.

On the browser of course, this is significantly lessened.  I've seen situations where an unhandled exception does threaten the execution of other "threads" (event-initiated call stacks), causing the UI to freeze.  But nowadays most browsers recover quite elegantly, so I haven't actually seen that in a few years.  And if you're using onerror, that would handle it quite well, like Bluebird's unhandledRejection event.

Glad to help :-)
~ Nate

domin...@gmail.com

unread,
Jun 15, 2016, 4:51:53 PM6/15/16
to bluebird
I wasn't aware, that devs should be so careful with throwing exceptions in node apps. It makes perfect sense not to use .done() there, then. Thanks for another explanation.

Nate Schmolze

unread,
Jun 15, 2016, 4:53:19 PM6/15/16
to bluebird
np!
Reply all
Reply to author
Forward
0 new messages