Promise Ponderings, (Anti-)Patterns, and Apologies
If you aren’t familiar with Promises or you have used them but don’t know why, then I hope this article will help you. Because “Promises are confusing.” And like any programming construct, having a good grasp of its behaviour comes from understanding what a Promise is built to do, not just how to use them. In this article I want to discuss some ways to utilise the “power of the Promise” and explain that in some cases unexpected or unusual behaviour is a feature of the language, not a “bug in the Matrix”. We will also discuss some common anti-patterns; what they are, why they are potentially dangerous, and how to avoid them.
Give us this day our daily Promise,
and forgive us our asynchronicity,
as we forgive those who nested too many callbacks,
and lead us not into Callback Hell,
but deliver us from evil,
for thine is the JavaScript,
and the power, and the glory,
for ever and ever,
ECMA
Glossary
1.0 Promises
2.0 Ponderings
2.1 “What do resolve
and reject
mean?”
2.2 “What is the difference between new Promise()
and Promise#then
?”
2.3 “What can I give Promise#then
? Does it accept a new Promise
?”
2.4 “What happens if I don’t resolve
a Promise
?”
2.5 “What affect does reject
ing a Promise have?”
2.6 “If I throw
an Error, where will it go?”
2.7 “Should I throw
an Error in a new Promise()
or call the reject
function?”
2.8 “Is throw
ing the same as reject
ing?”
2.9 “Why isn’t my Promise
‘s error handler being invoked?”
2.10 “Why is my Promise
‘pending’ when I have already resolved it?”
2.11 “Generally, what rules should I follow for handling errors?”
3.0 Patterns
3.1 Batching with Array#map
and Promise.all
3.2 Clean Chaining
3.3 Throwing Instead of Rejecting
4.0 Anti-Patterns
4.1 Deferring a Promise
4.2 Nesting Promises
4.3 The Non-Returning Promise
5.0 Apologies
6.0 References
1. Promises
Why? Asynchronous development means passing control between multiple contexts frequently. This results in quite confusing code, as anonymous function declarations dominate the codebase and make it difficult to follow. Promises are a way of organising asynchronous operations in such a way that they appear synchronous and we can therefore leverage them to save us from feared Callback Hell [1].
1 | new Promise(function(resolve, reject) { |
How? Promises are a part of the ES6 specification [2]. This means they are available natively in implementations of the spec or alternatively, for environments that haven’t caught up yet [3], in a plethora of third-party libraries [4]. My favourite is bluebird for its fantastic array of features and the famous promisify
function [5,6]. To run the examples given in this article I suggest copy-pasting into the Chrome developer console.
2. Ponderings
As humans we are all Ponderlings, each of us accustomed to a good old ponder now and again. It is the lifeblood of momentum in learning and carries us from bemused dumb-brains to clever-little-things. Here are some ponderings that I pondered as a Promise Ponderling… I hope that made sense… and I hope they will help you.
2.1 “What do resolve
and reject
mean?”
To understand what these terms mean we only have to consider why Promises borrow their name from their real-world counterpart, whose purpose is as an assurance mechanism. In the real-world a promise has two outcomes: that the assurance is fulfilled, or it is unfulfilled. We can map the actions resolve
and reject
as arbiters of each outcome, respectively:
ACTION => OUTCOME ------------------------- resolve => fulfilled reject => unfulfilled
2.2 “What is the difference between new Promise()
and Promise#then
?”
We can create a new assurance by using new Promise()
. The function that Promise
accepts will eventually call either the resolve
or reject
functions, which are given as arguments. Promise#then
is an instance method, allowing you to accept the result or failure of a Promise while itself returning a new Promise. As a result we can chain calls to then
, each invoked when its immediate ancestor has resolved or rejected. It is this chaining that cleverly brings asynchronous architecture back to the familiar linear behaviour of synchronous development.
1 | // Create a new 'assurance' |
2.3 “What can I give Promise#then
? Does it accept a new Promise
?”
The then
method accepts two callables: a handler for success and a handler for ‘catching’ errors (read on a few questions to find out what errors). Both callables are handed a single value on invocation. If you don’t give then
a callable the Promise will resolve immediately to the previous Promise’s value. new Promise(...)
is not a callable, and so this rule will come into effect.
1 | let promise1 = new Promise(function(resolve, reject) { |
2.4 “What happens if I don’t resolve
a Promise
?”
Then you take it to your grave! (Or to Promise Hell?) But really, nothing happens. Execution will not be passed to the next then
and your Promise chain will become ‘stuck’.
1 | new Promise(function(resolve, reject) { |
However don’t forget that inside a then
success handler, you can return any value to pass execution on to the next then
in the Promise chain:
1 | function helloWorld() { |
2.5 “What affect does reject
ing a Promise have?”
Rejecting a Promise will invoke the next available error handler supplied with then
.
1 | function rejectPromise() { |
2.6 “If I throw
an Error, where will it go?”
It’s up to you to give errors a place to go by supplying an error handler using then
. You can supply an error handler with each call to then
, but the handler will only be invoked when an error occurs before and outside of its own then
.
1 | new Promise(functiion(resolve, reject) { |
2.7 “Should I throw
an Error in a new Promise()
or call the reject
function?”
It depends. Throwing an error will stop execution within the function but calling reject
will not stop execution. However both will mark the Promise as ‘rejected’ so you cannot perform both and expect the error handler to be called twice. An error handler will only ever be called once.
1 | // Allow execution to continue by calling reject |
If you are calling a non-callback and non-Promise procedure that isn’t in your control and that throws an error traditionally, you can wrap it in a try/catch block. (Read the next question to understand why you cannot do this for a procedure that throws an error asynchronously - this will almost never be the case; it will instead ask for an error-first callback or return a Promise.)
1 | new Promise(function(resolve, reject) { |
2.8 “Is throw
ing the same as reject
ing?”
No. But yes. To the handler, there will be no difference, however you cannot throw
asynchronously! This is why we are given the reject
function to begin with! Placing an asynchronous operation within a try/catch does not guarantee that a thrown error will be caught by the catch block, because the asynchronous procedure will actually execute outside of the try block.
1 | // Attempt to catch an asynchronously thrown error using traditional try/catch block |
2.9 “Why isn’t my Promise
‘s error handler being invoked?”
A common mistake when first starting to use Promises is placing the handler for catching errors within the same call to then
where one of those errors may be thrown.
1 | new Promise(function(resolve, reject) { |
2.10 “Why is my Promise
‘pending’ when I have already resolved it?”
This is my most ‘pondiferous’ finding (or interesting for those less versed than I in the language of Ponderers): all Promise handlers are executed asynchronously! What does this mean? Let’s take a look…
1 | let p = Promise.resolve().then(new Array()); // (1) |
What’s going on?
(1) We create a resolved Promise using Promise#resolve
and attempt to create a handler to accept the success of this Promise, but give it a a non-callable. In this case, a new Array
.
(2) Then log the return of the statement (add both 1 and 2 to the call stack at the same time). This executes before the Promise attempts to call the success handler given to it with Promise#then
, as a Promise waits for the call stack to be empty before invoking all handlers.
(3) Finally, once the call stack is empty, the Promise resolves as it accepts the result of the call to Promise#resolve
because a new Array
is not callable.
2.11 “Generally, what rules should I follow for handling errors?”
Always catch errors using the Promise#catch
instance method at the end of any Promise chain.
1 | saveUser() |
And throw errors where they useful to the user or for debugging. If something has failed to save and you are auditing, for example, do you need to tell the user that it failed to save half-way through the process? Or just that it has failed to save altogether? Probably the former is most appropriate for debugging and the latter for the user.
3. Patterns
The Promise object has a number of static and instance methods that used together with methods available on other objects within the JavaScript global namespace (e.g. Array) allow us to create powerful aysnchronous behaviour. We call these patterns as these behaviours will be used multiple times within a single application and across any number of independent applications.
3.1 Batching with Array#map
and Promise.all
It is quite common to want an action performed after two or more other actions complete. Often you will be performing the same operation multiple times with different arguments.
How? You may have come across the Array#map
method already. It’s a great way of turning a collection of things into a collection of other things. As an example, let’s turn an array of incorrectly spelt words into an array of Promises that will ‘spell-check’ each word and return the correct word. We will then use Promise#all
to log the result of each Promise, but only once each Promise has been resolved.
1 | let promises = ['Wunt', 'Hullo', 'Sunt', 'Lut'].map(function(word) { |
3.2 Clean Chaining
Write clean code by chaining function references instead of giving anonymous handlers to then
. By giving a function reference at each step of the way you can make it much clearer what the code is doing to whoever is reading your code.
1 | validateNewUser(user) |
3.3 Throwing Instead of Rejecting
If you are writing large applications you might find it useful to start throwing errors traditionally instead of calling Promise#reject
when an error occurs. The best reasons I can think of for adopting this pattern are that (1) you can write custom Errors and (2) you don’t have to remember to call Promise#reject
.
1 | function validateUserCredentials(username, password) { |
4. Anti-patterns
Anti-patterns are the charlatan of the programming world - created when a particular programming tool or construct is used to solve the wrong problem whilst doing so in a semantic and seemingly ‘complete’ manner, they of course appear to lack any negative consequence, but in reality will have subtle and potentially harmful side-effects. Promises aren’t invulnerable to the lure of the anti-pattern, so here are some to be aware of.
4.1 Deferring a Promise
A common mistake made when starting out using Promises is wrapping a Promise-aware procedure in a defer [7]. This happened to me when I was made fimilar with deferreds before I was aware of Promises (even though Deferreds inherit from Promises!). The largest issue created by this anti-pattern is that errors become lost, meaning your application could fail unexpectedly or the user is left waiting indefinitely for an action to complete that has actually errored. Another is that you are over-complicating your code, potentially confusing other developers.
1 | function promisifyString(str) { |
The above is exactly the same as doing:
1 | promisifyString('Hello, World!').then(function(result) { |
This might seem obvious, but that’s because I have defined the function that we are wrapping. If you don’t know that the function you are inadvertently wrapping returns a Promise then this anti-pattern might catch you out.
4.2 Nesting Promises
Promises are made to take us away from nesting, but you will often find Promises nested within each other callback-style. The problem here is that it means by using Promises we are no longer solving the fundamental architectural issue from which they are borne.
1 | function makeHelloUniverse(str) { |
The correct way to approach this would be:
1 | promisifyString('Hello, World!') |
4.3 The Non-Returning Promise
To chain Promises you must pass a result by returning something from a handler: a value or another Promise. This may not be an issue if a Promise does not actually pass on any data, but it may cause some confusion for developers (or you, a month down the line) who do intend to pick up the result of that Promise later on. This is demonstrated quite easily if we rewrite our makeHelloUniverse
function.
1 | function makeHelloUniverse(str) { |
5. Apologies
Apologies to those who didn’t find an answer to their question here. Ask it in the comments and I will do my best to answer it. Or even better… ask StackOverflow [8]! If you’re starting out, lots of these examples will be confusing and may even make you feel like you’re taking a few steps back. Try using Promises ‘in the wild’, make mistakes, and you will soon start to understand them regardless of what you read on the web. Practically they appear difficult, but conceptually they are really quite simple.
6. References
[1] Callback Hell
[2] ECMAScript 2015: Promise specification
[3] ES6 compatibility table
[4] A comparison of JavaScript Promise libraries
[5] Bluebird Promise library
[6] Bluebird promisification documentation
[7] “Using Deferreds” from Q documentation
[8] StackOverflow ‘promise’ tag questions