Developing Async Sense in JavaScript — Part III: Promises

Nemanja Stojanovic
Enki Blog
Published in
11 min readOct 18, 2018

--

This series of articles is inspired by Kyle, Jake Archibald, and Douglas Crockford.

If you didn’t get a chance to, I suggest you read the previous articles in this series dealing with the concepts of sync vs async, threads and the Event Loop as well as callbacks and issues with them because this article will refer back to them.

What is a Promise?

One way to think of a Promise is as an eventual result of an asynchronous operation.

A Promise represents a wrapper around a future value.

Note: many programming languages call this construct a Future.

Promise vs Callback Analogy

A Promise is like that buzzer thingy you get in a restaurant.

As soon as you order your food, you are given this buzzer and you can continue to walk in and out of the restaurant, check your Twitter feed or go to the bathroom. You are not required to stand in a line and wait. Once your food is ready, you’ll be able to exchange the buzzer for it at your convenience. You can do it as soon as the food is ready, or sometime later when you finish what you’re doing. Regardless of when you actually go to get the food, your buzzer is a 1–1 replacement for it. Obtaining the food is in your control. You are not blocked while you wait for the food and you can immediately validate that you got what you ordered. Once the buzzer for food exchange is finished, the restaurant cannot affect your order any more.

On the other hand, callbacks work more in a way of you coming to the restaurant and instead of a receiving a buzzer you give them a giant tube that is connected to your stomach (tight coupling). In an ideal scenario, once the food is ready, the restaurant will send the food down the tube and then remove their access to it (it’s still on you to remove your side of the tube!). Similarly to having a buzzer, you are not blocked to wait in line for the food but you also don’t have any guarantees on how, when, or if the food reaches your belly; the restaurant is in control. They can, for example, keep the tube open and send another random and potentially harmful food item tomorrow and it is on you to protect yourself from this.

The buzzer inherently guarantees to be a single-transaction replacement for our previously selected specific food item, irrespective of when the food is ready. On the other hand, having a giant tube from the restaurant to our body is an accident waiting to happen.

The buzzer and the tube both enable us to get the food when it’s ready and not be blocked while waiting for it but the buzzer approach is safer, more predictable and reliable.

Now imagine that you made orders at multiple restaurants. Managing a few buzzers isn’t a big deal but having a bunch of potentially dangerous tubes attached to you seems impossible to manage and can easily lead to “tube hell” ;)

Ok, that may have been a bit of a stretch but you kind of get the point, right?

Let me elaborate.

Promises are inherently reliable because they resolve one time with a single value and become immutable. No external agent can alter the result of a Promise. They always return the same value, deterministically. They even propagate exceptions, natively.

With appropriate control-flow mechanism, Promises eliminate time as a concern in our programs. We can write code in a declarative fashion and pretend as if we already have the result, irrelevant of when it actually arrives.

Let’s see how all of this works with some code examples.

First thing’s first, how do we create a Promise?

We instantiate the Promise constructor and give it a callback that contains a function to call on success and another one to call on error.

How to create a Promise

Ok, and how do we use a Promise? We invoke the then method on a created Promise and pass it a callback to call on success, and a callback to call on error.

Only one of the callbacks above can ever get called.

It’s important to note that Promises are built on top of callbacks. They do not get rid of callbacks but instead wrap callbacks in a sensible and reliable mechanism.

A Promise that resolves successfully is denoted as fulfilled. A Promise that resolves with an error is denoted as rejected. The notion of “resolved” can imply either. It just means the promise has reached a final state.

Note: most of the time, we use the term “resolved” to mean fullfiled but technically it also encapsulates rejection.

Note: We can use Promise.resolve as a shorthand helper for a Promise that resolves with a value and Promise.reject for a Promise that rejects with a value.

Promises and the Event Loop

Each settled Promise creates a special Task in the Event Loop called a microtask.

Microtasks run at the end of the current Task of the Event Loop and do not create a whole new Task. Furthermore, microtasks can schedule additional microtasks within that same Task.

Each time we call .then on a settled promise, we immediately queue up a microtask.

Let’s look at an example:

As explained in the first article, JS Code runs on the main thread and in the highest-priority Task in the Event Loop. This means that the code above will execute within such Task.
During its execution, it will schedule a future creation of another Task by calling setTimeout. It will also create a microtask (within the same Task in which it is executing) and that microtask will schedule another microtask, again within that same Task.

This is why the entire Promise chain will execute before the setTimeout callback is called.

Note: This is also one of the reasons why the invocation of the setTimeout callback might take longer than the given millisecond timeout.

setTimeout is delayed because of a long chain of microtasks

Note: The code within the Promise constructor will run synchronously. Only the resolution itself (a call to resolve or reject) is asynchronous.

Promises fix the Non-Sequential Flow of Callbacks

How do Promises eliminate time as a concern and decouple the input from the output?

Take a look at the following snippet:

A Promise makes no assumptions on how or where we’ll process its result.

If a Promise generates a value before we try to obtain it, it will just hold it and we can grab it when we need it. If we try to obtain a value from a Promise before it generates it, it will call our callback passed into the .then method one time (at most — see caveats at the end) with the appropriate value, once its ready.

Promise generates a value only once
Promise always resolves with the same value
Promise generates a value once and always resolves with it from then on

Let’s now tackle the problematic code examples dealing with the non-sequential flow of callbacks (presented in the previous article) and demonstrate how Promises help us solve them in a more sensible manner.

Note: Try to solve any of the problems presented as you’re following along. It’s also suggested to first tackle the problems using callbacks (as demonstrated in the previous article). Doing so will help you to better actualize issues with callbacks and how Promises help us alleviate them.

The first example involved getting two values in parallel and processing them once both are obtained.

Note: As explained in the callbacks article, I’m using the term “parallel” here rather freely to mean “around the same time” as far as we care. Whether that actually happens in parallel behind the scenes isn’t as important (check out the first article for more specifics about parallelism and concurrency)

To get a value, let’s assume we have a function called httpGetPromise that takes a url, makes a request to it, and returns a Promise that will resolve to a value available at that url.

Stop here and try it yourself

Using Promises (and a nifty helper method Promise.all), we can solve it like this:

Note: here’s a code example of how Promise.all behaves. This is just an example. Since it’s available natively, we are not responsible for creating or maintaining it.

How does the data flow through this code?

There’s no need to manually maintain global state to communicate when both promises are done. We can just use an existing helper function Promise.all to encapsulate the management of the async state internally, allowing us to only care about our own logic.

What if we wanted to generalize this code to get any amount of values in parallel and process them once they are all received?

Stop here and try it yourself

Since Promise.all already accepts an array of Promises, we need to make minimal changes to generalize the code for any amount of urls:

The only thing that changed was the part of the code that sets up the number of values we want to handle, nothing else.

By using arrow functions and array-spread, we can further shorten this entire logic to a few lines:

Neat, right?

Here’s how that would look using callbacks (as shown in the previous article):

Promises are chainable

Another way Promises improve the control flow of async operations is by being chainable.

Every .then call returns a Promise. This means that we can chain multiple calls to .then and build a sequential vertical flow of Promise resolutions. In fact, because every call to .then returns a Promise, a Promise chain doesn’t even have to be joined together, it can be split up without altering its behavior.

Furthermore, if we return a Promise from the callback passed into .then, it will be inserted into the Promise chain.

We can even return a whole another Promise chain from the .then callback and it will be inserted at that point of the outer chain.

By allowing us to build these loosely-coupled chains, Promises enable us to respond to their results in any order we want, regardless of the order they actually resolve in.

This is great because we can pick how we want to process asynchronous results no matter when they arrive.

When you’re first starting out with Promises, it’s tempting to nest them similarly to how callbacks can lead to nesting.

Promises can also create deep nesting

This is actually an anti-pattern with Promises. Since every call to .then returns a Promise (and automatically propagates any errors), it’s much more readable to keep the chain vertical.

This vertical chain behaves exactly the same as the nested example above

Back to the code examples.

Let’s say we now want to get two values in parallel using the httpGetPromisefunction but process them in certain order, let’s say value1 then value2.

Stop here and try it yourself.

Here’s how we can do that:

The code above processes value1 then value2 irrespective of the order in which the Promises resolve in. The processing is controlled by how we build the Promise chain (input is separated from output), not how we create the Promises (and make requests).

value 1 received first
value 2 received first

Promises can be chained into a consistent Sequential Execution Flow, independent of the order of their internal asynchronous operations. We choose in which order to process results, no matter when they arrive.

How would we go about generalizing the code above for any amount of values? We want to request N values in parallel but process them in order (based on a given array of urls), without caring when they arrive.

Stop here and try it yourself.

We can combine the chaining capabilities of Promises with array reduction to dynamically reduce an array of Promises to a single Promise representing the chain. We do this by appending .then calls to each Promise down the chain, (using the looping mechanism provided by Array reduction) instead of explicitly typing the individual .then invocations.

This results in code that has a Sequential Flow:

It might help to visualize how this works. Here’s the Promise chain that the code above creates for 3 urls :

Solving Unreliability problem

As mentioned before, Promises always resolve once and in an immutable manner. This means that we can confidently predict that our success handlers will be called (at least) once (see caveats at the end).

What about errors?

We can propagate errors in a Promise chain by either:

  • explicitly calling reject when instantiating a Promise via its constructor
  • causing an exception within the Promise (some errors aren’t caught, see caveats)

Rejected Promises in a Promise chain are handled by the second callback parameter passed to .then.

Promises also give us some syntactic sugar to deal with errors. We can append a special .catch function to our Promise chain which (essentially) behaves like a .then with only the error handling callback.

Rejected Promises skip all success handlers in a Promise chain until an error handler is found.

Note: If there are no error handlers following a rejected Promise, the Promise is usually marked as an “unhandled rejection”.

As mentioned before, once a Promise resolves via fulfillment or rejection, it maintains that state. This means errors persists as well.

Promise rejection is immutable
Promise also propagates exceptions

Once an error is caught in a Promise chain, the chain can be continued normally.

Now let’s look at the problematic getJSON function from the callbacks article.

Here’s how the code, including more thorough error handling, would be written with Promises:

Awesome, right?

Promises are overall a major upgrade to a pure callback-based async code.

That being said, they still do not make async code fully intuitive. Just by looking at a Promise chain, the visual disparity between “now” and “later” still requires special syntax. Although they provide safer mechanisms and more consistent data flow, understanding a Promise flow is still a non-trivial cognitive effort. They don’t get rid of callbacks, instead, they make it tougher to mess up using them. They make it easier to do the “right” thing .

Perhaps there’s a construct in JavaScript that completely eliminates the syntactical split between sync and async code?

Next up: Developing Async Sense Part IV — Async/Await

Caveats

1. The term “resolve” encompasses both success or failure. This means that naming the first argument of the callback given to the Promise constructor, or naming the function Promise.resolve, as “resolve” is intentional. In fact, if we pass a rejected Promise to a resolve function, it will be added to the Promise chain and jump straight to any existing error handlers.
In other words, passing a rejected Promise into a resolve method creates a rejected Promise.

2. A Promise can actually never resolve. We could make a Promise never fulfill or reject by not calling any of the callbacks at all.

3. A Promise will not propagate errors thrown in nested async functions:

4. Error thrown in a success handler will be propagated down the chain and will not be handled by the error handler passed next to that success handler:

--

--

https://nem035.com — Mostly software. Sometimes I play the 🎷. Education can save the world. @EnkiDevs