[Node.js] Using callback-based functions when the rest of the code uses Promises
Gaurang
Posted on May 21, 2023
Hey!
My name is Gaurang and I've been working with Node.js for the past 5 years. This article is part of the "My Guide to Getting through the Maze of Callbacks and Promises" series. It's a 3-part series where I talk about writing asynchronous code with Node.js.
The articles are intermediate-level and it is expected that the reader already has basic familiarity with Node.js syntax and the constructs related to asynchronous programming in it. Such as callbacks, promises, and async-await.
Table of Contents
- Introduction
- Using util.promisify
- Creating a new Promise
- Create a Promise/callback dual-behaviour wrapper
- Conclusion
- Footnote
Introduction
Promises were introduced in JavaScript in 2015 - just 8 years back from the date of writing this article. Async-await was introduced in 2017 - just 6 years back. That was not a very long time ago. (At least not enough to eradicate the use of callbacks 😢)
As a working professional, you often need to work with legacy code. So as a JavaScript/Node.js developer, God forbid, you may have to work with callbacks at some point. And I'm assuming that you've had enough exposure to callbacks to understand the general dread that they bring.
It takes time to get a hang of writing code with callbacks. For those without prior experience with asynchronous programming, it is no different than bullfighting for the first time. For this reason, I believe they try to stick to the sync version of async functions. If you've read my previous article in the series, you would know that this approach is more damaging than it is helpful.
My personal opinion is that the best way to deal with callback-based functions is to convert them into promise-returning ones.
Suppose, you're working on a Node.js-based project. You created new modules (it could be APIs, controllers, utility services, etc.); everything using promises and async-await. However, there are some legacy functions written using the callback approach. You need to call one such function but don't want to give birth to a callback hell.
There are 3 ways of going ahead with this.
- Use
util.promisify
(My favourite) - Create a new promise.
- Create a smart wrapper that can get you either a callback or a Promise based on your requirement
Using util.promisify
A lot of people don't know about this gem that was introduced in Node.js version 8. If the legacy callback-based function uses the standard error-first callback style and the callback is the last argument, you can create a promise-returning function with minimal code using util.promisify
.
You have to be careful using it with functions that belong to a class/object though.
If you're new to the bind function and are not sure what the object context is, here are a few great resources that may help:
- MDN docs on bind function
- MPJ's explainer video on bind and this
- JavaScript Tutorial article on bind function
One of the best things about util.promisify
is that it works flawlessly with Node.js native library methods such as functions belonging to modules: fs
, child_process
, etc. You can use it to promisify setTimeout
as well.
Creating a new Promise
If the callback-based function doesn't follow proper conventions. Or for some reason, util.promisify
doesn't work for you, you can always create a custom new Promise.
You can move the Promise-creation to a separate function if doing it inline looks shabby.
Note: Here, promisifiedAsyncFunction
does not use async-await. But, it returns a Promise. So, we have to use await
in the main function while calling promisifiedAsyncFunction
If the callback-based function belongs to a class/object, you'll need to take care of the context.
Create a Promise/callback dual-behaviour wrapper
If you have access to the source code of the original callback-based function. And the authority to modify it, you can create a wrapper around it. This wrapper will work as a callback-based function if a callback is provided. If not, it'll return a Promise.
This dual-behaviour wrapper lets you work with Promises in new code that you write. Without disturbing the old code where the function is expected to work with a callback. Thus, it provides Promise support with backward compatibility for callback-based legacy code.
Previously, if the module exported the callbackBasedAsyncFunc
function, now it can export the dualBehaviourWrapper
.
// Previously
module.exports.callbackBasedAsyncFunc = callbackBasedAsyncFunc;
// Now
module.exports.callbackBasedAsyncFunc = dualBehaviourWrapper;
Thus, with minimal modification, you can add Promise support to a legacy module. And all the other existing modules, that are calling callbackBasedAsyncFunc
by passing a callback, remain unaffected. Win-win!
You might have noticed that a lot of popular NPM libraries give similar dual-behaviour support for their functions where they use a callback if it is passed, and return a Promise if a callback is not passed. Their implementation may be different though. For example, mocha.js
Conclusion
Overall, handling callback-based functions when the rest of your code uses promises can be a bit tricky. However, by following the tips in this article, you can make the process much easier. With a little practice, you'll be handling callback-based functions like a pro in no time!
I've put a lot of work into this article. And I hope that it has been helpful. If you have any questions, please feel free to leave a comment below.
I'd really appreciate it if you could like and share the article! 😊
Footnote
fs
version 10 introduced Promises API which has promise-based versions of functions. Have a look at the documentation. So you don't need to struggle with callbacks or manually convert fs
functions to Promise-based ones.
Posted on May 21, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.