DEV Community

Cover image for [Node.js] Using callback-based functions when the rest of the code uses Promises
Gaurang
Gaurang

Posted on • Updated on • Originally published at blog.gaurang.page

[Node.js] Using callback-based functions when the rest of the code uses Promises

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

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 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:

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;
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)