paint-brush
Function decorators: Transforming callbacks into promises and back againby@joelthoms
9,481 reads
9,481 reads

Function decorators: Transforming callbacks into promises and back again

by Joel ThomsMay 15th, 2017
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

Every day that I work in JavaScript-land, I stumble across a mixture of callbacks, promises or async/await. I have my own preferences in how I like to handle async code, though sometimes I don’t have a choice because an external library like <a href="https://nodejs.org/api/fs.html" target="_blank">fs</a>, <a href="https://serverless.com" target="_blank">serverless</a>, <a href="https://www.npmjs.com/package/aws-sdk" target="_blank">aws-sdk</a>, etc. is using something else.

People Mentioned

Mention Thumbnail

Companies Mentioned

Mention Thumbnail
Mention Thumbnail
featured image - Function decorators: Transforming callbacks into promises and back again
Joel Thoms HackerNoon profile picture

geralt @ pixelbay

Every day that I work in JavaScript-land, I stumble across a mixture of callbacks, promises or async/await. I have my own preferences in how I like to handle async code, though sometimes I don’t have a choice because an external library like fs, serverless, aws-sdk, etc. is using something else.

One thing I dislike in any codebase is inconsistency. So, if I start with promises, I’m going to use promises throughout.

This presents one minor issue; how can I maintain consistency in a project that has external libraries. Well, as the title of this article suggests, I transform them.

Callbacks and Promises

The most common scenario I run into is an older library using node-style callbacks that I would prefer to use with promises, or async/await, same stuff.

As a refresher, your typical node-style callback might look like this:

fs.readFile('./kittens.txt', (err, data) => {  if (err) throw err;  console.log(data);});

But this is how I want to access the method:

fs.readFile('./kittens.txt')  .then(data => console.log(data))

Often, I just punt and use promisify from bluebird.js. This is the easiest, fastest and laziest approach. But…

Importing a library to use but a single function is lazy and should be discouraged.

… especially when the function in question would be simple to write. (google leftpad broke the internet)

Stop right now and take a look at the size of your node_modules folder. It’s obscene! It is not uncommon to have a node_modules folder in excess of 1GB or even 2GB!

These things add up.

Promisify

What we want is called a Function Decorator.

A function decorator is a higher-order function that takes one function as an argument, returns another function, and the returned function is a variation of the argument function. source

Example of what a promisify function decorator would look like:

const readFile = promisify(fs.readFile)

readFile('./kittens.txt') .then(data => console.log(data))

The function readFile, still strongly related to fs.readFile, has been transformed into a variant of the original. This is a function decorator.

This should be easy to make. First we create the promisify function that takes 1 argument, func and returns a function. When that returned function is called it will return a promise.




function promisify(func) {return () =>new Promise((resolve, reject) => { })}

We used the keyword function above because we will need access to this, which is not available to arrow functions.

Notice (below) how one additional argument, callback is appended to the end of args. This is because our decorated function will not take a callback as an argument, but the original function func does.

func.apply(this, [...args, callback])

Put together it starts to look something like this:




function promisify(func) {return (...args) =>new Promise((resolve, reject) => {const callback = ???

  **func.apply(this, \[...args, callback\]);**  
})  

}

The final step also happens to be the easiest, create the callback. The callback is simply a node-style callback that will call either resolve or reject based on the presence of err.

Callbackify

I ran into a less common situation where what I needed was the reverse. The function I had to create needed to be a node-style callback function. Uggg, my code was already written using promises.

The code in question is for AWS lambdas, which are typically written using callbacks so my code ended up looking something like this:





module.exports.handler = function(event, context, callback) {myHandler(event).then(data => callback(null, data)).catch(err => callback(err))}

Because my library is all Promise based, I would much prefer to write something like this:


module.exports.handler = (event, context) =>myService(event) // note: I don’t need context.

I ended up creating the reverse of promisify, callbackify so I can create my lambda like this:


module.exports.handler = callbackify((event, context) =>myService(event))

Since we’re pretty much doing the same as above, we can zip through this a little quicker. Let’s create callbackify with a single argument,func, that returns a function. Since the function that is returned will take args + callback as arguments, we are going to have to separate them from each other.






function callbackify(func) {return function(...args) {const onlyArgs = args.slice(0, args.length - 1)const callback = args[args.length - 1]}}

Finally, we need to call apply on func and pass in our onlyArgs.

How does async/await fit into all of this?

async and await operates almost exactly like promises, so you can use them interchangeably.


const readFile = promisify(fs.readFile)const file = await readFile('./kittens.txt)

console.log(file)

Check that out, you can await a node-style callback function. Neat!

The Takeaways

Whether you use callbacks, promises or async/await, it is most important to be consistent.

Try creating your own functions before importing a library.

If you want to tweak how a library function works, create a function decorator.

Github

I put these functions up on github so that I can easily import them into my projects as I use them in almost every project. You are welcome to use them as well.


joelnet/functional-helpers_functional-helpers — Functional helpers_github.com

Related

util.promisify is being added to node

bluebird’s promisify

End

If you found this interesting, or are interested in functional programming, you might enjoy some of my other articles.


Latest stories written by Joel Thoms — Medium_Read the latest stories written by Joel Thoms on Medium. Computer Scientist and Technology Evangelist with 21 years of…_medium.com

I know it’s a small thing, but it makes my day when I get those follow notifications on Medium and Twitter (@joelnet). Or if you think I’m full of shit, tell me in the comments below.

Cheers!