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.
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.
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
.
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
.
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!
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.
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
util.promisify is being added to node
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!