Counting promises vs Rx

The difficulty using Promises vs the simplicity using reactive streams

I love using promises and have successfully used them in all sorts of situations. Yet I always had troubles when trying to connect multiple asynchronous steps together. One difficulty with Promises is that the first step should return a Promise instance, while the other steps could be just "regular" functions. Yet there is a deeper reason why putting promises together feels very difficult.

Each individual promise is a decision point. It can either succeed or fail. In some abstract sense, it is an "IF" statement created dynamically. Normally, an "IF" statement is hard coded in code by you - the programmer. Yet, a promise is an execution branch (it has a success path and the failure path) that is created at runtime. If writing a regular "IF" statement is governed by the JavaScript syntax, the rules of promises are much looser, and only the code execution can tell me if the code runs or not.

1
2
3
4
5
6
// regular IF statement
if (condition) {
// success path
} else {
// failure path
}

The above condition statement exists as soon as the JavaScript engine "compiles" or "evaluates" the loaded file. Yes, the "condition" might not be evaluated yet, but the "if / else" block is already part of the logic. On the other hand the code below does not have a similar clarity at the moment the code has been loaded.

1
2
3
foo()
.then(...) // success callback
.catch(...) // failure callback

When the above code is loaded, the above statement is executed yet - the foo() might NOT return a promise instance at all. It might return a Promise or it might return nothing, causing an exception. The execution fork itself does not exist until the foo() runs and returns something.

It is simpler to think about things that are known sooner, like "if / else" statements. They are also simple from the syntax point of view to connect. If we have multiple conditions, we can quite nicely write imperative code

1
2
3
4
5
6
7
8
9
if (condition1) {
// condition1 is true
} else if (condition2) {
// condition1 is false
// condition2 is true
} else {
// condition1 is false
// condition2 is false
}

When connecting multiple promises together, it requires a lot more boilerplate code. Each Promise needs some code to be created, plus chaining them together and passing the right parameters into each function requires additional code. In fact, if we have N async operations, each will require a separate promise, thus making the amount of boilerplate code proportional to N.

Example

We can see this quite nicely if we instrument code before running to get the code coverage information. Imagine we have a simple program that does the following

  • takes a number from an array
  • adds a constant asynchronously just to spice things up
  • prints the result.

important I want to implement the operations sequentially - not run all asynchronous operations in parallel. This will force us to chain promises instead of using just Promise.all. A good real world example of sequential async actions would be "fetch information about N users without making 100s of parallel requests, but one at a time".

First, let us implement the solution using promises.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
'use strict'

function add(a, b) {
return Promise.resolve(a + b)
}

const numbers = [3, 1, 7]
const K = 10
// add K to each number and print
function mulAndPrint(x) {
return add(K, x)
.then(function (sum) {
console.log(sum)
})
}
Promise.resolve()
.then(() => mulAndPrint(numbers[0]))
.then(() => mulAndPrint(numbers[1]))
.then(() => mulAndPrint(numbers[2]))
.catch(console.error)
/*
13
11
17
*/

The amount of promise-related boilerplate code is quite large - it is because we need a promise for each asynchronous addition (inside the artificial add function), plus extra code for chaining each promise to the previous one.

Let us get the coverage information: install nyc and run the program to get the HTML report.

1
2
$ npm install -g nyc
$ nyc --reporter html node promises.js

Look at the counters below: each promise has to be created for each number, thus the amount of boilerplate is proportional to N items in the input array.

promises

Our input array has 3 items. Thus every place that has counter value "3x" has its execution number proportional to the number of data items. Line 4 is the only one that should be executed 3 times - it actually adds two numbers together. Everything else is the boilerplate code that makes it more difficult to reason about the program! Especially interesting is the code connecting the promises sequentially in lines 17-19. It took 3 lines to connect 3 promises, thus the chaining of N promises itself is proportional to the number of items.

Example implementation using a reactive stream

Here is the best solution for fighting the promise-induced bloat: reactive programming. Instead of handling each item as a new decision point (by creating a promise instance and linking it to the previous promises), a reactive program establishes a single stream of items, and the library's code takes care of connecting async operations. Instead of being a dynamically created branch, each item can only pass in the stream, and the error handling (the "else" path) is taken care at the stream level.

The same program implemented using RxJs library shows much simpler coverage profile

reactive

Notice that every line aside from the inside of the function add has only been executed once! We built the reactive "pipe" where the events (the numbers) are moving, one after another. Thus instead of "building" the dynamic computation by linking promises inside our code, we just built a single rail line and let the data cars run to the destination.

The single stream initialization, independent of the data that is going to travel along it also explains why every variable is declared const in the code. The stream "infrastructure" we created for the events to move along is immutable - there will be no new "branches" or "side streams" created ever while the program is running.