loading...

Pro tip: using Promise.then for function composition

shalvah profile image Shalvah ・1 min read

I've become a fan of functional programming in recent times (even though I still don't know much about it). Over time, I've developed a habit when working with Promises in JavaScript. I try to construct my code so it ends up reading like this:

let tweetsToAttendTo = fetchTweets()
  .then(removeDuplicates)
  .then(removeTweetsIveRepliedTo)
  .then(fetchRelevantTweets);

This means I'm using then not only to control flow but also to pipe output of one function to the input of another (thereby creating a pipeline). I find that this pattern makes the code easier to read and reason about, and gets rid of unnecessary filler like the arrow function and extra variable in the following:

fetchTweets()
  .then(tweets => removeDuplicates(tweets));

Be careful though! There are a few things you must know when using this pattern:

  1. Applying this method depends on the result of the previous function being the input to the next function
  2. Be careful when using object methods. For instance, the following two snippets of code aren't the same thing:
getModels().then(r => manager.applyFilters(r))

// any calls to `this` in `manager.applyFilters` will return undefined
getModels().then(manager.applyFilters)

Ultimately, don't force it. I use this often, but if it just isn't working (output of F1 isn't input of F2, F2 behaves differently, I need to do special error handling), I let it go. Remember, no silver bullets!

Posted on by:

shalvah profile

Shalvah

@shalvah

Writer and builder. APIs, dev tools, automation. Advocate of simple design.

Discussion

markdown guide
 

The cool thing about Promises is that the then method, when used this way, is just like the map function. It has the same abstract type signature. This makes Promises behave like any other functor, which means that this method respects composition. Therefore,

somePromise
  .then(f)
  .then(g)
  .then(h);

is the same thing as

const w = pipe(f, g, h);
somePromise.then(w);
 

Yes, I was thinking that as well. When chaining a sequence of maps, it's better to only map once using a composition of functions.

 

I'm curious as to why you think it's better to "map once". I personally prefer using "then" repeatedly, because it might take a while for people who aren't familiar with functional programming to grab the concept of piping.

 

YES! I wish more people started to use similar patterns to this usage, but I still see a lot of deferreds and new Promise() for standard promise usage
Quite a good read Shalvah A!

as a side note:

I remember finally looking at the light with promises after reading several articles, but the one which clicked on me was this

pouchdb.com/2015/05/18/we-have-a-p...

In it includes one example of your usage, which in turn is part of a quiz in promises, ultra recommended lecture if someone doesn't understand promises

 

@shalvah this article is fantastic! I've been exploring the ergonomics of the pattern (particularly around promises) for a little while now... I wrote up an example-rich github project to outline the refactor process: github.com/justsml/escape-from-cal...

@Angel, that pouchdb article is amazing, I make all my students read it :D

 

The only issue I see here is that promise is always asynchronous and using then for function chaining might compromise shared data.

 

I'm not sure I understand you. Could you give an example of what you mean by "compromising shared data"?

 

I mean that each time you chain a function using then that function is added to the queue which means that it will wait for at least a tick to be called, my only concern is if during that tick a variable, that one of the callbacks relies on, is modified. Maybe is not a pressing matter, just a concern I have.

Nice post tho.

@Pichardo Promises resolve only once, and their value is immutable. And in a functional promise chains each step results in a read-only state.

 

It's called tacit programming or point free. I love doing it too

 

Right. I recall seeing that in a book on FP. Limiting the number of points.

 

I've done a couple of functions like this, but still I haven't solved the problems of

  • testing it
  • how do you find out where exactly crashed on production and with what input?
 
  • Testing: I don't think this is particularly a problem. You test them the way you test regular functions. Write unit tests for the individual functions and write a test for the entire pipeline.
  • Error handling: You can intersperse a couple of .catch blocks in there to handle specific errors during execution. But the problem would be when you need to exit the chain if an error occurs midway through. In such a case, I don't recommend using this approach, as that would lead to a lot of crazyyyy code. For uncaught errors, the stack trace would typically contain the name of the function that failed.

I hope I've addressed your concerns. Did I miss something?