Promises: The Definitive Guide

Not as Powerful as You Think

I don’t want to name names, but a lot of articles I read about async in JavaScript, especially those that came out around the time ES2015 (ES6) was new, are just plain wrong. I’m going to clear up the falsehood by presenting a real understanding of the way async works in JavaScript.

This series of articles is based on a slide from a talk I did about Redux-Observable. I wanted to take it one step further and explain how async works and why it’s a foundation to coding in JavaScript.

The Least Capable Method

Promises are amazing, but extremely limited. I’ve done a lot with promises over the years even before they became popularized as part of native JavaScript in 2015. My favorite thing about them is how they made async so much easier to grasp. It fixed callback-hell and reduced the indirection caused by promises by providing a linear API you could read from top to the bottom.

Kinda.

So why are promises the “least capable”? Because they have so many drawbacks. As you use promises, you’ll see more of these drawbacks and long for fixes. And while I’ve used Bluebird before, it doesn’t fix all the issues; it just makes promises more versatile.

Confusion

Back in the day, a lot of different promise implementations existed. I was most-familiar with the jQuery version and highly customized it to do quite a bit more like firing data through the pipeline multiple times. It didn’t follow the A+ spec, but eventually did provide the methods necessary to do so in 1.8.

AngularJS was the same way. Back when it came out, it used a fundamentally different methodology from what we know today as promises. Version 1.6 opened up the doors for A+ promises and version 1.7 did away with the old methods entirely!

And you’re probably familiar with, or have heard of, Bluebird. It’s a fantastic library that adds a lot of functionality currently missing from the ES2015 implementation of promises. Even the ES2018 version only adds Promise.prototype.finally ; nothing like what Bluebird makes available.

And again, strictly speaking in promises, Bluebird is the library for you. But there aren’t just promises out there. Other async methods exist in JavaScript and while promises are great, they’re superseded by observables. Still, when push comes to shove, the A+ Promise is native to ES2015 which holds a clear hat to anything requiring a 3rd party library like observables.

To Resolve or Reject? That is the Question.

Not really a question anyone’s ever asked in their lives. What are these methods, and how do I access them?

If you’re creating a promise from a callback, you’ll new up a Promise like so:

new Promise takes a callback so just like with callbacks, you need to put your try - catch code in the callback rather than outside of it.

Surprisingly, when creating a new promise, not only are you giving it a callback, but you’re also given two callbacks in your callback function. This is what I was referring to in Callbacks: The Definitive Guide: everything async in JavaScript is based on callbacks. resolve gives you a thenable output and reject throws an error into the promise chain but doesn’t return an error. More on that in the next section.

I rarely new up Promise though. Instead, I opt for the semantic Promise.resolve and Promise.reject . Sometimes they don’t fit your needs like in the example above or if you’re converting from a callback, but they’re fantastic for debugging and super useful when you need to create promises from flat values. It’s like calling the resolve and reject callbacks directly.

They look a little something like this:

Thenable

Promises are referred to as being “thenable” which means you can chain a bunch of then statements and each one receives the return value of the previous callback. In my previous article on callbacks, you’ll remember I stated it is possible to get the return value of a callback. So instead of calling another callback, if you keep your callbacks synchronous, you’ll be able to grab the return values off of them.

This is exactly the process a promise’s then function uses. It takes a callback (also known as a handler), and executes it synchronously, takes that return value, and passes it to the next then on the chain. If any of those callbacks error, it will trek through the chain until its finds a catch (or the second argument of a then ), and call its callback method.

What Happens After `catch`?

This is the weird part for most folks. I guarantee most JavaScript developers don’t know this information.

If you throw an error from a catch and there’s another catch somewhere down the chain, the second catch will get that error. But let’s say you don’t throw an error and instead just console.error . Too bad, now you’re going to hit the next then in the chain.

So while it’s pretty cool you can continue processing on an error, the worst part is that you keep processing forever and ever and ever. There’s no way to stop a promise as any then will be called unless you’ve gone through all of them and any that error will hit a catch which could either proceed to the next catch or then .

This has nabbed me in the past and is one of the core reasons I stopped using promises in production code (now I use observables). This is worsened if you’re entire app is made of up a long chain of many promises being passed around. It can get pretty hairy, but at least your app won’t stop processing! 👺

Two `then` Args or `catch`?

The then method on a promise can take two arguments. I only ever recommend giving it the one because the second argument is literally the same as following a then with a catch .

Take these two examples for instance:

They’re exactly the same thing, but one reads better, is more maintainable, and is less-likely to be missed when debugging. Trust me, you want as little crazy-code in your way when you’re dealing with complex async bugs.

If, for whatever reason, you have very long anonymous functions in your then , you’ll miss the second argument everytime. That’s all the devs on your team, including you in 6 months.

Honestly, do what works best for your team in your project, but I prefer the semantic catch over the non-semantic double-arg then , and I really think you should too. I’d never allow another dev to use the double-arg version without a ridiculously good reason.

Returning Promises

To avoid callback-hell, it’s important to note that then can handle returning both a value and another promise. When returning a value, it’s passed to the next then , but when returning a promise, it actually switches your next then to the value that promise passes down the chain; not the promise itself.

This is how that looks:

Notice how you can return both resolved and rejected promises, and they’ll hit the next then or catch just like normal. This is a powerful feature as it allows you to consistently keep your promises tabbed to the left for the most part.

Callback-Hell

I’ve actually seen people write callback-hell with promises like so:

And then, as a smart promise dev, you’ll go in and fix it:

The issue comes when you have scoping problems. For instance, you might’ve been using the server and connection objects in your message function since you need to know which connection to send a response back to and which server was called. Even though you cleaned up the code, you lost the scope of the previous values.

There are a few ways to solve this, but one of the cleanest (kinda) might be to just combine them all into a new object each time:

I’ve used this methodology before but never have been fond of it. It comes in handy when you need to split things up because your then callbacks are getting too large. While it allows you to avoid callback-hell, it’s arguable if it’s really any better.

This is the exact same situation I talked about in Callbacks: The Definitive Guide. Callback-hell isn’t necessarily a bad thing. Even though you’re told promises get around it, that’s not really the case.

Anti-Patterns

Just like with callbacks, promises don’t work with try - catch , not the way most people would use them.

Here’s an anti-pattern you’ll commonly see:

This should be written as:

Why is this? Because promises are always async. There’s absolutely no way to make a promise synchronous like you can will vanilla callbacks. This is important to know because not using the Promise.prototype.catch method will get you. I usually come across these types of issues in Node.js applications since most browser developers often use Promise.prototype.catch .

Finally

Back before ES2018, we didn’t have the much-needed finally method, now we finally have one. In a simplistic way, all this method does is add syntactic sugar around a pairing of then and catch , but without the values being passed. This is similar to the finalize operator in RxJS which, sadly, also takes no values. Considering the amount of promise code I’ve seen causing problems without it, I’d say it’s a welcome addition to ES2018.

I’m going to say it again, finally callbacks do not receive values. It’s not like tap in RxJS, it’s a method that only helps with cleanup no matter if the code succeeded or errored.

Here’s how it looks:

It might not be what you were expecting, but every time a finally is passed, it executes. So if you put it before a bunch of chained then and catch statements, it will execute before them. Unless then , it will also execute in the event of an error. Usually those pass down to the next catch , but they will also execute all the finally functions along the way.

In real-world applications, if you don’t have finally , it’s possible you could be creating a bunch of memory leaks and never-ending loading indicators etc etc. This happens because most people never finish off their promises with a catch and only use then . If your promise ever errors and your last then removes your loading indicator, it’ll never disappear.

While finally doesn’t have to be at the end of a promise, having it allows you to use it in place of a then and ensure that your app cleans up after it’s done.

Try-Catch-Finally

If you didn’t know yet, try - catch also has a finally method. It looks like this:

In this case, it has to be in the order of try , then catch , then finally . It’s possible to leave out the catch or finally entirely, but not both.

As you can see, it will execute regardless of if you hit try or catch . Now, why does this exist? You could very-well just log after your try - catch statement instead of using finally . And that’s the important bit. If your catch throws an error, then your execution will halt right then and there and your console.log would never occur. finally to ensures you can still execute something even after throwing an error.

Then why wouldn’t you just put that code in catch instead of finally ? Because you also want that code in your try . This prevents code deduplication and ensures your code always executes as long as that try - catch - finally statement exists.

Promises use finally more as a cleaner helper function, but in both cases, you still might have a database connection open which you need closed. This is where it would be executed (yes even after throwing an error in catch ) to close off that connection. It’s indispensable in these types of scenarios.

All for Race and Race for All

If you didn’t know, there are two other methods hidden away in the promise API: Promise.race and Promise.all . Both of them are methods for handling an Array of promises — more specifically, an iterator of promises but let’s not go there.

All

Promise.all takes that list of promises, waits for all of them to return a value, and send off an array of those values to the next then callback:

Make sure, when using Promise.all , that you pass in an array of promises as it only takes one argument. Your results come back also in an array where each value is the resolved value from the previous one.

If any, and I mean any one promise fails, your entire Promise.all is in an rejected (errored) state, and your error message is only from the first erroring promise. If you have more than one erroring, you have no way of knowing.

Race

Promise.race takes that list of promises and takes the first one to emit a value. If you have a bunch of them, the others will be essentially canceled:

This is probably the most-common use-case for Promise.race . I haven’t used this operator very often myself, but it also has some gotchas. First, you have to remember to pass it an array just like Promise.all . Second, if one of these promises errors, it doesn’t matter unless that error emits first.

So if you have two promises, one succeed and the other errors, you’ll get the success value. If you have two promises and one errors before the other succeeds, your Promise.race will be in the rejected state. This is a really big quirk and probably the cause of many hours of frustration for folks that use Promise.race .

Bluebird

There are plenty of promise libraries out there, but by far the most popular, and probably the one you’ll actually see used in production, is Bluebird. It’s a fantastic promise library with lots of features, but unless you’re in the Node.js space, you probably won’t see it.

And JavaScript native promises aren’t Bluebird promises so you’d have to convert them.

There are quite a few benefits you gain from using the library with the best one being the Bluebird.promisify function.

Personally, I don’t recommend following Bluebird’s official best practices for importing it in your project, and I’ve written an entire article about it for your benefit:

Bluebird’s Bad-Practice Docs

The Many Drawbacks

Everything I’ve talked about is mostly pros, but the I subtitled this article with the notion that promises kinda suck. Why? What could be so bad when they look way better than callbacks?

One and Done

First, they can only be fired once. Callbacks are superior to promises for this very reason as they can handle events over time. Promises suffer being limited to a single emission and that’s a killer flaw I’ve run up against on many occasions. I think it comes up just about anytime I’m working with promises in large-scale applications.

Debugging is Painful

Console logging (tapping into the stream) is tough. Debugging promises sucks. While you’d think Promise.prototype.finally is just what we were looking for, it doesn’t receive any data from the promise chain so it can’t be used for logging purposes. Bluebird doesn’t have a solution for this either.

One of the most-powerful things about RxJS observables is your ability to tap into the stream at any point in its operation. Every Promise.prototype.then is the equivalent of both a map and switchMap where you can return either a value or another promise. Sadly, this modifies the value being passed in the chain and requires more-complex logic to get some debug statements in there, logic that can cause maintenance headaches if done incorrectly.

The cheat way I do it, only when debugging, is like this:

It Keeps Going and Going…

Promises don’t stop executing. This is a problem when catching errors and then ing after. They go through the chain until there’s nothing left and have absolutely no way to stop mid-way.

You could assign a “stop processing” value to the promise object itself, but then you’d either have to extend the Promise class or wrap every then , catch , and finally in a “check if the promise is stopped” function.

Immediate Execution

Promises execute immediately on creation. While it may not be obvious, if you have a setTimeout in a promise declaration, that will execute as soon as it exists rather than waiting for your first then to exist. If you’re not expecting it, this can do some damage. The only way around it is creating a function that then returns the promise wrapping the setTimeout .

Never Synchronous

Promises are never synchronous even with Promise.resolve . This means they can’t be subbed in as a native pipeline operator while we wait for the official ES implementation.

More on this in my article:

An Emoji-Lover’s Guide to Functional Programming: Part 4

Doesn’t Completely Remove Callback-Hell

In the same way callbacks require closures to get rid of callback-hell, promises need to map return data into a more-complicated object. Because of variable scoping, it’s easy to wind up in a similar callback-hell as callbacks.

Difficult to Pass Around Promises in `then`

If you actually want to pass a promise — not its value — over to the next then , you’ll have to wrap it in an array or object so it doesn’t get automatically converted. Most people won’t realize this and will probably spend a few hours trying to figure out why their code isn’t working. I know I have.

Semantics

then isn’t semantic. Sure, it’s semantic to say “this happens, then this happens”, but that’s not what I mean. Each function you pass into a promise’s then is either anonymous (ie unnamed) or is a named function which exists somewhere else in your application. This causes the same indirection you’d find in callbacks making promises only slightly better because of the linearity of the operations.

Conclusion

If all I have is promises, I’ll use them. They’re much-preferred to callbacks in my book and were a huge step to helping me manage async in JavaScript for the 3 years I was stuck with them, but in everything I’ve written over the last year and more, I’ve instead opted for observables as they’re well and beyond the complete package of what anything you could expect from promises.

The simpler your promises, the easier they are to work with. The longer your promises and the more you pass them around your app, the harder to trace and less reliable they become.

Do I like promises?

Yes.

Do I think observables are better?

Of course.

If I’m using callbacks, will I promisify them?

Probably not anymore.

More Reads

I’ve got more of these async articles coming! The previous one was on callbacks:

Callbacks: The Definitive Guide

If you liked what you read, please checkout my other articles on similar eye-opening topics: