DEV Community

Cover image for Adapting Rusty Old Callbacks to Shiny Async Functions
Geoff Davis
Geoff Davis

Posted on

Adapting Rusty Old Callbacks to Shiny Async Functions

While writing code for a side project I stumbled upon some use cases for the recent async/await feature in Javascript/Node.js, which led me to write this article.

Continuing on in that project, I realized that the library I was using to interact with my database was using callback functions; this isn't the worst pattern in the world, but I was writing a wrapper class around the API and found it clunky sending the results of a query from the deeply-nested callbacks. Ultimately, it works fine, but I wanted a more elegant solution that is easier to follow.

I then got to thinking "what if there was a way that I could wrap some callback-utilizing functions in a way that would let me use asynchronously while also keeping my code DRY".

And from that spark, asyncAdapter was born.

The Problem

Let's say your has a function that creates an XMLHttpRequest which passes response data to a callback function, for example:

function httpGET(endpoint, callback) {
  var xhr = new XMLHttpRequest();
  xhr.addEventListener("readystatechange", function() {
    if (this.readyState == 4 && this.status == 200) {
      callback(JSON.parse(xhr.responseText));
    }
  });
  xhr.open("GET", endpoint);
  xhr.send();
}
Enter fullscreen mode Exit fullscreen mode

It has been a trusty sidekick, but it is a little outdated and makes using retrieved data more involved than modern Javascript needs to be.

You want to use the latest and greatest APIs that tc39 and Babel can provide– like async/await or the Promise API– and callbacks just don't cut it.

What could you do?

My Solution

Enter asyncAdapter. This nifty utility magically makes the function into a new Promise-based function, allowing it to be await-ed or otherwise handled like a Promise; this is achieved by passing in the Promise's resolve argument where the original function's callback would go.

(Okay so it is not exactly magic, but it's still pretty cool)

Here is how you would use the example function above with asyncAdapter:

const asyncHttpGET = asyncAdapter(httpGET, "https://example.com/api/data");

(async function someAsyncFunction() {
  const data = await asyncHttpGET;
  console.log(data); // -> { foo: 'bar' }
})();
Enter fullscreen mode Exit fullscreen mode

The first argument to the adapter is the original function name, and the rest of the arguments constitute any arguments you would pass to the original function, in the same order.

Note that you should not pass a function in the callback parameter's position into the asyncAdapter arguments, unless that function can return a value (e.g. not for an AJAX/Promise-based function).

Here is an example of a non-asynchronous function being used with asyncAdapter:

// Original function
const add = (n1, n2, callback) => callback(n1 + n2);

// Add callback function to return value
const asyncSum20 = asyncAdapter(add, 10, 10, n => n);

// Add callback function to return value with side effects
const asyncSum50 = asyncAdapter(add, 10, 10, n => n + 30);

// Use inside function to create DRY async version of original function
const asyncSum = (n1, n2, n3) => asyncAdapter(add, n1, n2, n => n + n3);

(async function someAsyncFunction() {
  const sum20 = await asyncSum20;
  const sum50 = await asyncSum50;
  const sum100 = await asyncSum(5, 20, 75);

  console.log(sum20); // -> 20
  console.log(sum50); // -> 50
  console.log(sum100); // -> 100
});
Enter fullscreen mode Exit fullscreen mode

I have found that this implementation is fairly flexible and provides some benefits of functional programming when wrapping the adapter in a function (like the asyncSum function above).

Do note that this may not work 100% out of the box for every third-party callback-based function; because asyncAdapter depends on the callback argument being the last set in the function parameters, this pattern may end up being more valuable to those who can apply it to their own codebase's functions and have control over those functions' parameter order.

Conclusion

Does this sound like something you could use? Or perhaps a fun utility function to play around with?

You're in luck! I just published this to the npm registry here.

Install it with your favorite npm client...

npm i async-adapter

yarn add async-adapter

Find an bug or have an idea for a feature? File an issue or submit a pull request.

I hope you enjoy the package and find it useful. Thanks for reading!

Further Reading

Top comments (3)

Collapse
 
danielescoz profile image
Daniel Escoz

Or you could use util.promisify without having to install a single library.

Collapse
 
geoff profile image
Geoff Davis

Definitely one way you could go!

asyncAdapter can work in Node.js or browser js files, whereas util.promisify is a node-only option.

Collapse
 
danielescoz profile image
Daniel Escoz

True, I always forget the browser. At that point I would probably use Bluebird's promisify, but only because I love Bluebird. This option is a nice save for the browser.