-
Notifications
You must be signed in to change notification settings - Fork 1
Concepts: differences between Promise, Deferred, and FastDeferred
At the moment of this writing (2016), promises prove to be an invaluable tool to handle asynchronous operations. Yet they reveal numerous practical problems in this field. The list below applies to the standard promises as well as most existing shims.
Standard promises defined by ES6, yet not implemented across all popular platforms:
- Only latest versions of node.js support promises.
- Most browsers do not support promises yet.
The promise definition requires running callbacks and errbacks in a separate time slice.
Browsers do not support time slices directly (safe for setImmediate()
on IE10 and up). Such functionality has to be approximated with time delays, which introduce ... time delays that can be compounded in certain situations affecting responsiveness. Following techniques can be used:
setTimeout()
postMessage()
requestAnimationFrame()
The techniques above in reality are completely indeterminate, because their actual delay depends on visibility of a web application window, rendering tasks at hand, complexity of CSS, and other hard-to-estimate factors.
It is extremely difficult to debug asynchronous problems partly because delaying code to the next time slice removes a valuable context: a call stack. While we know that something is wrong with a state, we don't know how we arrived to this state.
It can be difficult to debug code when even unintended exceptions are converted to rejected states. An exception is appeared to be "swallowed", manifesting itself in some unexpected places.
Converting exceptions to rejected states require a try
/catch
pair, which is known to prevent JIT on some platforms, e.g., V8 of node.js and Chrome.
Missing functionality to support asynchronous functions:
- Our asynchronous code may stop waiting for an asynchronous operation to complete. Use case: user decided to switch to a different page without waiting for a lengthy I/O to show all items on this page, user doesn't want to finish long processing delegated to a server, and so on. We should be able to cancel such operation to free precious resources, or at the very least to communicate that we are not interested in the result (other clients of the same promise may be still interested in it).
- Long asynchronous operations are required to provide a feedback on their progress, so user can make informed decisions on waiting vs. canceling. Some experts advise to use a different mechanism for that, e.g., event streams, but doing so has two major drawbacks:
- We have to recreate a topology already created with promises.
- It doubles API, and doubles the code. Shipping more code to browser, especially on mobile networks, affects the overall performance negatively, especially clients with cold cache.
- It may introduce a close coupling between a producer and a consumer of progress events.
With the current Promise API in place most common use cases require "patterns", "boilerplate", which results in cut-n-paste "techniques", mistakes by omission, and so on.
- Promise is designed to host a transformation chain, while the most common use case is an event-based handler propagating forward the same value, not a transformation chain. The decision to host a transformation chain makes handlers context-dependent, and require an advanced knowledge of their predecessors in the chain, and their transforms. Any change in a handler may require a cascade of changes in handlers down the chain.
- Users frequently forget to return a value from callbacks:
// the most frequent use case: return the same value var chain1 = promise.then(function (value) { doSomethingUseful(value); return value; // important! }).then(/* ... more chaining ... */); // the most frequent mistake var chain2 = promise.then(function (value) { doSomethingUseful(value); // nothing is returned! "return undefined" is assumed }).then(function (value) { // value is undefined here! }).then(/* ... more chaining ... */);
- Users frequently forget to rethrow errors:
// the most frequent use case: rethrow var chain1 = promise.catch(function (error) { doSomethingUseful(error); throw error; // important! }).catch(/* ... more chaining ... */); // the most frequent mistake var chain2 = promise.catch(function (error) { doSomethingUseful(error); // nothing is thrown! "return undefined" is assumed }).catch(function (error) { // we are expected to get here, but no-o-o! }).then(function (value) { // instead we land here, and value is undefined! }).catch(/* ... more chaining ... */);
- Rethrowing is expensive, and may prohibit JIT optimizations, yet it is a common "pattern".
-
then()
andcatch()
are to mimictry
/catch
blocks. But where isfinally
? The frequent use case is to release resources after an asynchronous operation is finished, or do some other necessary actions (clean up related resources, notify a user, and so on). Typically it doesn't matter what value was returned, or how an operation finished: normally, or with errors.// implementing "finally" function cleanUp () { /* ... */ } var chain1 = promise.then(cleanUp, cleanUp). then(/* ... more chaining ... */); // did you notice a mistake? we did not return or rethrow any value... // implementing "finally": the "right" way var chain2 = promise.then( function (value) { cleanUp(); return value; }, function (error) { cleanUp(); throw error; } ).then(/* ... more chaining ... */);
- Users frequently forget to return a value from callbacks: