Xavier LaRue | April 11, 2017 | 10 Min Read
Get the code and a walkthrough of each step below.
It’s fairly straightforward, but there are some not insignificant problems, which will become obvious as we examine the code.
In lines 10 through 27, various functions are created, each one having a callback that is passed in, a fairly standard JS program structure. These functions define the interface. Following this we use the interface starting at line 29.
Before running the code, what are our expectations for its output? We might reasonably expect that we get the ‘app starting’ message printed out, followed at some point by the ‘done’ message. We also might reasonably expect that when ‘data’ is printed out (line 41) that it contain the data retrieved from the ‘DB’ (actually generated by the faker library, but that’s not important). This is a reasonable expectation given the apparent sequence of calls to the interface.
However, when we run the code, the following is output (at least on the machine I’m running the code on):
At which point we may well ask: “Where the heck is the data”, or “Why is the data empty?”
When we look again at the call-flow we see that connectToADb takes a callback, sets a timeout and calls the callback using the db parameter that was passed in; getData does much the same thing except that it returns a random word and processData creates an array of random words, waits for a random amount of time and calls the passed in callback. Ultimately, line 37’s “data = args” is ignored, because by the time that is called, data has already been printed out, i.e. before the callbacks returned.
Unfortunately, this is one of the chief problems with callbacks: creating functions, within functions, within functions each having its own scope leads to unclear code — unclear code is unmaintainable code — unmaintainable code is death to a project. In this artificially simple example it is possible to see what is going on, however, when we consider ‘real’ code, the problems increase geometrically.
In this simple example, data is a string, but if it were an object the problems would be even greater. It would unclear in which scope an object reference was being modified, what sequence it was being modified in, etc.
Given the previous, what are promises?
“A promise represents the eventual result of an asynchronous operation.
It is a placeholder into which the successful result value or reason for
failure will materialize.” — Wikipedia
“The Promise object is used for asynchronous computations. A Promise represents a value which may be available now,
or in the future, or never.” — Mozilla docs
*Promises have analogs in many programming languages (Java – see ‘Futures’, C# – see ‘Tasks’)
Promises allow you to concentrate on what each step of a process should do, rather than worrying about in what order these steps should be called.
A promise is an object that represents a placeholder for the eventual result of an operation. Promise has ‘then’ and ‘catch’ methods which correspond to the possible results, both success and failure. The caller may provide callback functions that will be triggered when the promise enters the resolved (i.e. success) or rejected (i.e. failure) states.
Promises are chainable: ‘then’, ‘catch’ and any other function on a Promise return a Promise. The key point being that the chained order is deterministic. The next element in the chain will not be called unless the prior promise has resolved. As each is resolved, the result is passed to the next callback function.
A Quick Word About Promise States
pending | the initial state, neither fulfilled or rejected
fulfilled | successful completion
rejected | unsuccessful completion
A promise is resolved if trying to resolve or reject it has no effect, i.e. the promise has been “locked in” to either follow another promise or has been fulfilled or rejected. Note: resolved is not a state, but a statement about state 😉
–see also: https://github.com/domenic/promises-unwrapping/blob/master/docs/states-and-fates.md
Lines 14 – 24: Promise Creation
In this instance we are creating two simple Promises; one that arbitrarily resolves and one that equally arbitrarily rejects.
Construct a promise by passing a ‘resolve, reject’ handler to the constructor function. Note this function is called immediately. The resolve and reject functions, when called, resolve or reject the promise. Typically the handler does user-defined work and calls the resolve or reject functions based upon that work. Also, if any error is thrown by this handler function, the promise is rejected. Note: when rejecting, the user may either call the reject function or throw an error, they are equivalent:
Image via Mozilla
Applying these principles to the first example we end up with:
Structurally code that uses promises constructs an initial promise and then appends 1..n ‘then’ (and or ‘catch’ — more later) functions. (see lines 30-33)
As we can see the control flow sequence is easily followed and doesn’t require the same mental gymnastics to be performed as the callback equivalent. A core design principle here is to make each step as simple as possible, these functions should be idempotent and use only the values passed into them. It could be said that the use of promises is a natural application of the standard (and ancient — in software terms) principle of functional decomposition.
Look at the definition of ‘a()’, it has a number of ‘then’ clauses as well as a ‘catch’ clause. If this code is run we get the following output:
The ‘then’ clauses in lines 26-28 are not called. This is because, following the failure case, execution proceeds to the first ‘catch’ clause in the chain. As discussed above, it makes no difference whether the promise explicitly calls reject or an exception is thrown, in either case, program flow is transferred to the first catch in the chain.
If the ‘catch’ successfully handles the exception the chain will continue. Not all exceptions and errors are recoverable, but if the ‘catch’ clause is able to handle the error, the chain will proceed to the next ‘then’ in the promise chain.
Designing With Promises
Designing with promises requires somewhat of a paradigm shift, but a helpful one in that it introduces clarity to the code. Rather than unintentionally obfuscating control flow, in that operations that appear to be sequential are not due to the use of callbacks. Instead, promises naturally lead to each function being separate and complete in itself. Of course, it is possible to wrestle against this — it is, after all, only code. However, designing with promises lends itself to a stricter separation of functions and hence concerns.
Promises are ideally suited to situations where there is an asynchronous call flow. Promises introduce a more deterministic call flow whereas callbacks can lead to indeterminate behavior. Error handling and recovery is also supported by promises. This allows for simpler callflow in many scenarios. For example, given a function that handles login credentials, it may be appropriate to have error handling functionality that permits adding a user — or that allows the user to retrieve their credentials. In all successful cases continuing with the same behavior: a successful login follows the same result callflow whether the user logged in normally, a new user was created or even recovered their credentials.
Integration with the non-promise world
Code that makes extensive use of promises that wishes to integrate with code that follows the old callback based model (i.e. most 3rd party libraries out there) can use various adapter libraries that will ‘promisify’ (sic) wrapping callbacks in a promise form. A simple google search will reveal any number of such libraries.
This will return a promise that is already resolved to ‘1’. This allows one to avoid going through the whole formal construction of a promise as seen in the prior examples.
Takes an array (or promises) and returns a promise that will resolve when all the included promises are resolved. The returned promise will have an array of the results of each of the promises in the order in which they appear in the initial array. There are no guarantees as to what order the promises will complete, but the order in the results array will be the same as the order of the initial promise array.
Promise.all if it fails, will fail fast, it will fail as soon as it encounters a promise that fails.
Takes an array (or promises) and returns a promise that will resolve if any of the included promises is resolved. Perhaps ‘any’ would have been a better name than ‘race.’ There is only a single result, rather than the array returned by promise.all.
Promise.any does not fail fast, because a failure of a single promise doesn’t necessarily mean that the others will all fail. It will only fail if all of the included promises fail.
Promise.all does not guarantee the order in which the included promises are called, if such a guarantee is required, it can be enforced by setting each promise’s ‘then’ member to be the next promise in the array. This can be simply achieved using the ‘lodash’ library (see below).
For example, given an array of Promises ‘promises’
Gotcha: Regarding Promise.all and Promise.any behavior in the cases where they either ‘fail fast’ or ‘succeed fast,’ this is true only from the perspective of the caller. All promises included in the initial array will run to completion, however in the case of Promise.all any results obtained after a failure will be discarded, and in the case of Promise.race any results obtained after the first success will be discarded. I.e. promises are non-cancellable*.
In the case of Promise.all this may lead to unforeseen side-effects because even if a failure has occurred other promises will continue. If these promises include server-side interactions — the server may think there has been a successful interaction, but the client code would be unaware. Perhaps your client code has some error handling code which will be called in the failure case, but meanwhile, the original promises will be continuing on their merry way.
*This may be changing and is certainly supported in some 3rd party implementations (e.g. Bluebird)
Get Email Updates
Get updates and be the first to know when we publish new blog posts, whitepapers, guides, webinars and more!
Guide to Creating Engaging Digital Health Software
This guide shares our knowledge and insights from years of designing and developing software for the healthcare space. Focusing on your user, choosing the right technology, and the regulatory environment you face will play a critical role in the success of your application.Read More
Accelerate Time To Market Using Rapid Prototyping
In this webinar, you will learn how to leverage rapid prototyping to accelerate your products time to market in one week, agile sprints.Read More
WebRTC: Top 5 Unified Communications Systems Integration Challenges
WebRTC is looking to be a game changer in terms of its impact on voice and data communications.Read More