Codementor Events

Maybe We Should Keep Our Promises

Published Jan 22, 2018Last updated Jul 21, 2018
Maybe We Should Keep Our Promises

More and more I see JavaScript that makes use of Async/Await:

async function imperativeWay(color) { 
  try { 
      let resultsString = await fetch(color)
      let jsonResults = JSON.parse(resultsString)
      let transformedResults = transformFirst(jsonResults) 
      let sendResults = await sendToClient(transformedResults) 
      await remotelog('log', sendResults) 
      } 
    catch (e) {
      remotelog('error', e) 
    }
}

Up until last year, we would have written the above using promises:

function functionalWay(color) {
  fetch(color)
  .then(resultsString =>JSON.parse(resultsString))
  .then(jsonResults => transformFirst(jsonResults))
  .then(transformResults =>sendToClient(transformResults))
  .then(sendResults => remotelog('log',sendResults))
  .catch(err => remotelog('error', err))
  }

We fetch some data from an API and pass it a parameter, convert the returned string to JSON, transform the result, and send the result to a client. Finally, we log the result to a remote logging service one way if all is well, and another way if an exception is thrown. 

Both boil down to a series of functions executed in order, each passing a result to the next: a pipeline. Another name for a pipeline is a functional composition.

Composition is an important technique, if only because composed code is easy to read, reason about, and evolve.

So, if some sequence of operations can be expressed in terms of functional composition, it should explicitly be written that way. The first version of our code somewhat obscures the fact (though as we’ll see, it’s probably somewhat better written than we’d actually find in the wild). 

A promise chain is already a composition (and often the first time a developer is explicitly forced to think in such terms). As a core part of the language, you must deal with them as part of everyday practice (and not in some fantasy-land extension to the language). I’m afraid that in doing away with them, people are losing out on the opportunity for experiences that will make them better coders.

So, in praise and memory of promises, we’ll transform the promise-based version into something like the following that is even more explicitly composed:

const superFunctionalWay(color) { 
  pipeP( fetch,
    JSON.parse,
    transformFirst,
    sendToClient,
    remotelog('log')
    )(color)
  .catch(remotelog('error'))}

Not everything can be reduced to this pattern! But in languages that support first class functions, a good deal of application code can and should look like the above.

We’ll dig into the details in a later post (If you are having trouble, compare the above promise-based version which is pretty much equivalent).

Lesson 1: What’s the Point?

Back to the original promise-based function. Let’s remove all unnecessary anonymous functions:

function functionalWay(color) { 
  fetch(color)
  .then(JSON.parse) 
  .then(transformFirst)
  .then(sendToClient)
  .then(sendResults => remotelog('log',sendResults))
  .catch(err => remotelog('error', err))
}

People who don’t do this — who wrap a callback function of a certain arity (number of parameters) with another function with the same arity — haven’t fully taken to heart that functions are first-class entities in JavaScript. They forget that the basic job of then in a promise chain is to eventually call the function passed into it with a result.

The code is easier to read without the anonymous functions and we eliminate the need to create parameters with meaningful names. Naming things is difficult and and in this case, they are redundant. Without them, you start to see the flow of data through the composition.

Letting your functions be called in this way is called point-free or tacit programming and is the subject of religious controversies. Judge for yourself which version of the code is easier to read.

At the very least, after learning this lesson, we’ll never again write the sort of code that litters countless production code bases.

db.find({id}, (err, res) => handleResults(err, res));

Maybe we notice something a bit more profound — that we never need to call a function ourselves in a promise chain as long as it takes a single argument (has an arity of 1). Maybe we start thinking about when and why anonymous functions are required, like for our logging statements.

Lesson 2: Don’t Break the Chain

Something is eventually going to go wrong. Maybe, under certain circumstances, the API endpoint returns some differently shaped data or it’s not clear just what transformFirst is doing. You want to see what's going on.

It’s trivial to do this with the imperative version. Just console.log any intermediate variable.

async function imeperativeWay(color) { 
  try { 
    let resultsString = await fetch(color) 
    let jsonResults = JSON.parse(resultsString)
    console.log(jsonResults) 
    let transformResults= transformFirst(jsonResults) 
    let sendResults = await sendToClient(transformResults) 
    await remotelog('log', sendResults) 
    } 
  catch (e) { 
    remotelog('error', e) 
  }
}

But you can’t just introduce a console.log into the promise chain, not in a point-free manner, or in an anonymous function:

function functionalWay(color) { 
  fetch(color)
  .then(_in => console.log(_in))
  .then(JSON.parse)
  .then(console.log)
  ...
  }

That’s because console.log doesn’t return a value. It takes some rather heavy lexical manipulation to accomplish logging in a promise chain:

function functionalWay(color) { 
  fetch(color)
  .then(_in =>{
    console.log(_in) 
    return _in }) 
  .then(JSON.parse) 
  ...}

Ugh! I often hear having to do and undo the above is a big reason why folks don’t like to work with promise chains. I don’t blame them, especially when there’s a linter involved. 

But eventually you write (or are gifted) a function that looks like this:

function inspect(_in) {
  console.log(_in)
  return _in
}

It takes an argument, logs it to the console, and returns the original argument. With this reusable function, we can debug any and every promise chain like so:

function functionalWay(color) { 
  fetch(color)
  .then(inspect)
  .then(JSON.parse)
  .then(inspect)
  .then(sendToClient)
  .then(sendResults => remotelog('log',sendResults))
  .catch(err => remotelog('error', err))
}

Here we catch a glimmer of a new way of thinking about and organizing the building blocks of our applications. Something more than thinking imperatively in terms of constructs, like loops and conditional statements, and something less, perhaps different than thinking in terms of classes and inheritance hierarchies.

We start thinking as functions as the primary units of work. Adding a new behavior, logging in this case, is simply a matter of inserting a new composable function into the chain.

inspect is just a specialized version of a function that takes a non-composable function and returns a composable one, often called tap.

function tap(fn, _in) { 
  fn(_in) return _in
}
function inspect (_in) {
  tap(console.log,in)
}

Once we write a tap function, we need never write it again. At this point, some of you might get a sense of the seductiveness of the FP Kool-Aid, write a function once, and use it forever. 

Maybe..., but as a next step let’s dig a little deeper into the functions that make up our pipeline. You can play with the code we’ll be using here.

Discover and read more posts from Robert Moskal
get started
post commentsBe the first to share your opinion
Cedric Poilly
6 years ago

Thank you Robert! Was very insightful.
The code is more readable when using promises and chaining .then calls. The “tap pattern” is something I’ll use and keep using for long 👍

Show more replies