[Node.js] Using callback-based functions when the rest of the code uses Promises
Tags: node, javascript, programming, coding
Published: 2023-05-21 12:08:58

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](#intro)
- [Using util.promisify](#util-promisify)
- [Creating a new Promise](#new-promise)
- [Create a Promise/callback dual-behaviour wrapper](#dual-behaviour-wrapper)
- [Conclusion](#conclusion)
- [Footnote](#footnote)

# Introduction <a name="intro" \>

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.
1. Use `util.promisify` (My favourite)
2. Create a new promise.
3. Create a smart wrapper that can get you either a callback or a Promise based on your requirement

# Using `util.promisify` <a name="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][err_first_cb] and the callback is the last argument, you can create a promise-returning function with minimal code using `util.promisify`.

{% gist https://gist.github.com/gaurang847/d2e9265a2b2a6d5a3a72cbb942c31dea file=promisify_simple.js %}

You have to be careful using it with functions that belong to a class/object though.

{% gist https://gist.github.com/gaurang847/d2e9265a2b2a6d5a3a72cbb942c31dea file=promisify_with_bind.js %}

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][mdn_bind]
- [MPJ's explainer video on bind and this][mpj_bind_this]
- [JavaScript Tutorial article on bind function][jstut_bind]

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 <a name="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.

{% gist https://gist.github.com/gaurang847/d2e9265a2b2a6d5a3a72cbb942c31dea file=new_promise.js %}

You can move the Promise-creation to a separate function if doing it inline looks shabby.

{% gist https://gist.github.com/gaurang847/d2e9265a2b2a6d5a3a72cbb942c31dea file=new_promise_new_func.js %}

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.

{% gist https://gist.github.com/gaurang847/d2e9265a2b2a6d5a3a72cbb942c31dea file=new_promise_new_func_with_class.js %}

# Create a Promise/callback dual-behaviour wrapper <a name="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.

{% gist https://gist.github.com/gaurang847/d2e9265a2b2a6d5a3a72cbb942c31dea file=dual_behaviour_wrapper.js %}

Previously, if the module exported the `callbackBasedAsyncFunc` function, now it can export the `dualBehaviourWrapper`.

```javascript
// 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][mocha_js]

# Conclusion  <a name="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 <a name="footnote" \>
`fs` version 10 introduced Promises API which has promise-based versions of functions. Have a look at the [documentation][fs_documentation]. So you don't need to struggle with callbacks or manually convert `fs` functions to Promise-based ones.


[mdn_bind]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind
[mpj_bind_this]: https://youtu.be/GhbhD1HR5vk
[jstut_bind]: https://www.javascripttutorial.net/javascript-bind/
[err_first_cb]: https://fredkschott.com/post/2014/03/understanding-error-first-callbacks-in-node-js/
[mocha_js]: https://mochajs.org/#asynchronous-code
[fs_documentation]: https://nodejs.org/docs/latest-v10.x/api/fs.html#fs_fs_promises_api