tutorial // Jul 08, 2022

How to Wrap an Asynchronous JavaScript Function with a Promise

How to write a callback-based function and then convert it to be a Promise-based function that can be called using async/await.

How to Wrap an Asynchronous JavaScript Function with a Promise

For this tutorial, we're going to use CheatCode's full-stack JavaScript framework, Joystick. Joystick brings together a front-end UI framework with a Node.js back-end for building apps.

To begin, we'll want to install Joystick via NPM. Make sure you're using Node.js 16+ before installing to ensure compatibility (give this tutorial a read first if you need to learn how to install Node.js or run multiple versions on your computer):

Terminal

npm i -g @joystick.js/cli

This will install Joystick globally on your computer. Once installed, next, let's create a fresh project:

Terminal

joystick create app

After a few seconds, you will see a message logged out to cd into your new project and run joystick start:

Terminal

cd app && joystick start

After this, your app should be running and we're ready to get started.

Writing a callback-based example function

To begin, we're going to write a function that uses the traditional (dare I say "old school") callback function pattern that was popular before JavaScript Promises arrived. In the project that was just created for you when you ran joystick create app above, in the /lib folder, we want to add a new file sayHello.js:

/lib/sayHello.js

const sayHello = (name = '', options = {}, callback = null) => {
  setTimeout(() => {
    const greeting = `Hello, ${name}!`;
    callback(null, greeting);
  }, options?.delay);
};

export default sayHello;

Above, we're writing an example function called sayHello that uses a callback-pattern for returning a response when it's called. The reason a callback may be used is because the function we're calling needs to do some work and then respond later. Using a callback, we can prevent that function from blocking JavaScript from processing additional calls in its call stack while we wait for that response.

Here, we're simulating that delayed response by calling to setTimeout() in the body of our function. That setTimeout's delay is dictated by options we passed to sayHello() when we call it. After that delay has passed and the timeout's callback function (here, the arrow function being passed to setTimeout()) is called, we take the name passed to sayHello() and concatenate it into a string with Hello, <name> !.

Once that greeting is defined, we call the callback() function passed as the final argument to sayHello passing null for the first argument (where the consumer of the function would expect an error to be passed—an undocumented "standard" among JavaScript developers) and our greeting for the second.

This is all we need for our example. Let's make some better sense of how this is working by putting this to use and then move on to converting sayHello() to be Promise-based.

Calling the callback-based example function

Now, we're going to open up a file that was already created for us when we ran joystick create app above: /ui/pages/index/index.js.

/ui/pages/index/index.js

import ui from '@joystick.js/ui';

const Index = ui.component({
  render: () => {
    return `
      <div>
      </div>
    `;
  },
});

export default Index;

When you open that file, we want to replace the existing contents with the snippet above. This will give us a fresh Joystick component to work with for testing out sayHello().

/ui/pages/index/index.js

import ui from '@joystick.js/ui';
import sayHello from '../../../lib/sayHello';

const Index = ui.component({
  events: {
    'click button': async (event, component) => {
      sayHello('Ryan', { delay: 3000 }, (error, response) => {
        if (error) {
          console.warn(error);
        } else {
          console.log(response);
        }
      });
    },
  },
  render: () => {
    return `
      <div>
        <button>Say Hello</button>
      </div>
    `;
  },
});

export default Index;

Expanding this out, we've done two things:

  1. In the HTML string returned by the render() function at the bottom of the component, we've added a <button></button> tag between the existing <div></div> tags that we can click to fire our function.
  2. To handle the firing, just above render(), we add an events object and define an event listener for a click event on our button tag.

To that event listener definition 'click button' we assign a function which will be called when the click event is detected on the button. Inside, we call to our sayHello() function which we've imported up top. Calling that function, we pass the three arguments we anticipated when writing the function: name as a string, an object of options with a delay property, and a callback function to call when our "work" is done.

Here, we want our function to say Hello, Ryan! after a three second delay. Assuming everything works, because we're using console.log() to log the response to sayHello in our callback function (we expect this to be our greeting string), after 3 seconds, we should see Hello, Ryan! printed to the console.

Callback example
Our callback function being called after 3 seconds by sayHello. (Low-Res)

While this works, it's not ideal, as in some contexts (e.g., having to wait on multiple asynchronous/callback-based functions at one time), we run the risk of creating what's know as "callback hell" or infinitely nested callbacks in order to wait on each call to complete.

Fortunately, to avoid that, JavaScript Promises were introduced into the language and alongside them, the async/await pattern. Now, we're going to take the sayHello() function, wrap it in a Promise, and then see how it can clean up our code at call time.

Wrapping the callback-based function in a Promise

To write our Promise-wrapped version of sayHello, we're going to rely on the methods feature of Joystick components. While this isn't necessary for this to work (you could write the function we're about to write in a separate file similar to how we wrote /lib/sayHello.js), it will keep everything in context and easier to understand.

/ui/pages/index/index.js

import ui from '@joystick.js/ui';
import sayHello from '../../../lib/sayHello';

const Index = ui.component({
  methods: {
    sayHello: (name = '', options = {}) => {
      return new Promise((resolve, reject) => {
        sayHello(name, options, (error, response) => {
          if (error) {
            reject(error);
          } else {
            resolve(response);
          }
        });
      }); 
    }
  },
  events: {
    'click button': async (event, component) => {
      const greeting = await component.methods.sayHello('Ryan', { delay: 3000 });
      console.log(greeting);
      // sayHello('Ryan', { delay: 3000 }, (error, response) => {
      //   if (error) {
      //     console.warn(error);
      //   } else {
      //     console.log(response);
      //   }
      // });
    },
  },
  render: () => {
    return `
      <div>
        <button>Do the Thing</button>
      </div>
    `;
  },
});

export default Index;

Here, we've added another property to the options object passed to our ui.component() function called methods. The object assigned here allows us to define miscellaneous functions that are accessible elsewhere in our component.

Here, we've defined a method sayHello (not to be confused with the imported sayHello up top) which takes in two arguments: name and options.

Inside of the function body, we return a call to new Promise() to define a new JavaScript Promise and to that, we pass a function which receives its own two arguments: resolve and reject. Inside, things should start to look familiar. Here, we're calling to sayHello, relaying the name and options passed to our sayHello method.

The idea here is that our method is going to function like a "proxy" or remote control for our original sayHello function. The difference is that for the callback function, notice that we take in the possible error and response from sayHello, and instead of logging them to the console, we pass them to either reject() if there's an error, or, resolve() if we get a successful response back (our greeting string).

Back down in our click button handler, we can see this being put to use. We've commented out the callback-based version of sayHello so we can see the difference.

In front of the function passed to click button, we've added async to signify to JavaScript that our code will be using the await keyword somewhere inside of the function being passed to click button. If we look at our refactor, we're doing exactly that. Here, from the component instance automatically passed as the second argument to our event handler function, we call to component.methods.sayHello() passing in the name string and options object we want to relay to the original sayHello function.

In front of it, we place an await keyword to tell JavaScript to wait for the Promise returned by our sayHello method on the component to resolve. When it does, we expect the greeting string to be passed to resolve() which will be stored in the const greeting variable here (in this example, three seconds after calling the method).

Finally, once we get back a result, we console.log(greeting). What's nice about this is that we've not only streamlined our code, but we've simplified it enough so that we can call it alongside other Promises without having to nest a bunch of callbacks.

Wrapping up

In this tutorial, we learned how to take an existing callback-based asynchronous function and wrap it with a JavaScript Promise to make calling it use less code and play nicely with other Promise-based asynchronous code. We learned how to define the original callback-based function and put it to use discussing its disadvantages, and then finally, learned how to use Joystick's methods feature to help us define our Promise-based wrapper function.

Written By
Ryan Glover

Ryan Glover

CEO/CTO @ CheatCode