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.
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.
Create a new promise.
Create a dual-behaviour wrapper that can get you either a callback or a Promise based on your requirement
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
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:
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.
promisifiedAsyncFunction does not use async-await. But, it returns a Promise. So, we have to use
await in the
main function while calling
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
// 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
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! 😊
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.
Did you find this article valuable?
Support Gaurang Pansare by becoming a sponsor. Any amount is appreciated!