ES6 Promises – 5 Things I Wish I’d Known

Over the past couple of months I’ve started shifting my development focus away from PHP to Node.  Node is a huge departure from PHP development and I’ve uncovered quite a few gotcha’s along the way.

One of the major differences is the Asynchronous nature of JavaScript.  If you’ve ever wondered why people say the MEAN stack is quick, this is why.  Where PHP will run all code in sequence, JavaScript will run multiple requests at once, asynchronously.

Although this makes end requests a lot faster, it can lead to callback hell . If you’ve spent any time writing JavaScript then you’ve probably come across the terms callback hell or the pyramid of doom, a phenomenon which can lead to some pretty unwieldy code.


originalFunction(
firstCallback(
secondCallback(
thirdCallback(
fourthCallback(
fifthCallback(
)
)
)
)
)
)

Promises are the ES6’s answer to this problem, allowing you to chain your functions and make your code much easier to follow.


originalFunction()
.then(function(output) {
return firstPromise(output);
})
.then(function(output2) {
return secondPromise(output2);
})
.then(function(output3) {
return thirdOutput(output3);
})
.then(function(final_output) {
// Finally, do something
});

I’ve spent a lot of my time writing wrappers to existing npm modules to work with a promise based approach. It’s been an emotional journey full of pain and joy. Here are a few things I wish I’d known before I’d started.

Tip 1 – Forget .defer().

Coming at Promises from Angular, I’d developed the bad habit of using the $q.defer() approach. The official implementation takes a much simpler approach. Using new Promise to create a Promise will pass two functions into your callback which will allow you to resolve or reject the promise.


return new Promise(function(resolve, reject) {
GetSomething(function(err, res) {
if (err) {
// Catch this using .catch()
return reject(err);
}

// Will pass through value to .then()
resolve(res);
});
});

Tip 2 –You can resolve a Promise straight away.

If you have a Promise with no possibility of failing, you can resolve it straight away using Promise.resolve()


Promise.resolve(100)
.then(function(output) {
console.log(output) // 100
});

Tip 3 – You can return another promise.

This is probably the most important thing to remember and one that has caused me a few headaches. When I first started, I found myself slipping back into my own variation of callback hell, ending up with something like the following.


// Welcome to Promise Hell...
return new Promise((resolve, reject) => {
promise1()
.then((output) => {
promise2(output)
.then((output2) => {
promise3(output3)
.then((final) => {
console.log(final)
});
});

});
});

This is terrible. Not only does it make things just as complicated, you’ll need to remember to apply .catch() to every nested promise, otherwise debugging can become a nightmare.

As well as manipulating a value within then(), you can also return another Promise. Not only is this a lot more readable, but an error at any point during the chain will be caught by the final .catch() call.


// Instead do this
function promise1(prev) {
return new Promise((resolve, reject) => {
output.push('Promise 1');
setTimeout(() => {
resolve(output)
}, 500);
});
}

function promise2(prev) {
return new Promise((resolve, reject) => {
output.push('Promise 2');
setTimeout(() => {
resolve(output)
}, 500);
});
}

function promise3(prev) {
return new Promise((resolve, reject) => {
output.push('Promise 3');
setTimeout(() => {
resolve(output)
}, 500);
});
}

let output = [];

promise1(output)
.then((output) => {
return promise2(output);
})
.then((output) => {
return promise3(output);
})
.then((output) => {
console.log(output); // ['Promise 1', 'Promise 2', 'Promise 3']
})
.catch((error) => {
// Something went wrong
});

Note: Make sure you’re using return if you need to use the data, or the promise needs to be resolved before continuing.

Tip 4 – You can wait for multiple promises to complete

By using Promise.all(), your Promise will wait until all promises have been resolved before continuing. Promise.all will return an array of values based on the promises passed into Promise.all().


Promise.all([
getUser(),
getPost(),
'CREATED' // You can also pass through regular variables
])
.then(function(output) {
var user = output[0];
var post = output[1];
var relationship = output[2];

user.relateTo(post, relationship);
});

It’s also important to note that if you are dealing with an array of records that need an action performed on them before continuing, you should be using Promise.all() instead.


db.getPostsByUser('adam')
.then(function(posts) {
return Promise.all(posts.map(function(post) {
db.delete(post.id);
}));
})
.then(function() {
// Now it is safe to delete the User
});

Using a simple forEach on the posts array in this example will delete each post, but could cause issues if the action does not succeed before the next stage is processed. Using Promise.all() ensures that all actions are completed before the next stage begins.

Tip 5 – .catch() is shorthand

The catch() function is a shorthand for then(null, ...). However, there are two important gotcha’s here. Firstly, consider the two following snippets of code. Although they look familiar, they are not equivalent.


db.delete(record)
.then(function success() {
// Record has been deleted
}, function error() {
// There has been an error previously
});

db.delete(record)
.then(function success() {
// Record has been deleted
).catch(function error() {
// There has been an error at some point
});

The main issue when using the then(resolveHandler, rejectHandler) format, is that the rejectHandler will not catch an error if it is thrown within the resolveHandler, meaning you would need to add an extra level below to catch this error.


db.delete(record)
.then(function success() {
throw new Error('Something has gone wrong')
}, function error() {
// 'Something has gone wrong' will not be caught here
})
.catch(function(error) {
// ...instead it will be caught here
});

The second gotcha here is that using the then(resolveHandler, rejectHandler) format will not halt the chain.


db.delete(record)
.then(function success() {
throw new Error('Something has gone wrong')
}, function error() {
// 'Something has gone wrong' will not be caught here
})
.then(function(res) {
console.log(res); // Error: 'Something has gone wrong'
});

For this reason I always tend to omit the second argument for then() and instead use a final catch() at the end of the chain.

Conclusion

Promises are a great feature of Node JS, but getting your head around them can be a nightmare. If I had known these things before I’d began it would have saved me a few headaches. Hopefully this will save someone else’s sanity.