The problem
If you have been programming in Javascript for a while, you may have run into some code that looks like the following
const example = (url, options) => {
makeApiRequest((response, error) => {
if (error) console.log(error);
console.log(response);
// do something with response
}, url, options);
};
This code uses a callback function to perform an asynchronous task.
The problem with writing asynchronous code like this is maintainability.
- Code written below the call to
makeApiRequest
is executed before the code written in the callback function - Our additional arguments (
url
andoptions
) appear after our callback function, which can be difficult to find - If we need more asynchronous code within in our callback, we will end up further indented
- Error handling can become a total nightmare
In the olden days, this is how asynchronous code was written, and as a result of that, there are many libraries and methods that use callback functions like this, so it's fairly likely that you will run into async code using callback functions like this.
However, since the formal adoption of ES6, a better form of asynchronous code has become standardized: Promises. Promises allow us to write asynchronous code that is much more maintainable and easy to follow. If we could magically convert the above code to be Promise-based instead of using callbacks, it might look like this:
const example = (url, options) => {
makeApiRequest(url, options)
.then((response) => {
console.log(response);
// do something with response
})
.catch((error) => console.log(error));
};
This solves most of our maintainability issues with callback functions, and can be made even better with the Async/Await syntactic sugar standardized in ES8.
Using this syntax on our magically Promisified makeApiRequest
method would look like this:
const example = async (url, options) => {
try {
const response = await makeApiRequest(url, options);
console.log(response);
// do something with response
} catch (error) {
console.log(error);
}
};
Using async
and await
allows us to write code that looks synchronous, but actually performs asynchronous operations. Additionally, it prevents us from accidentally writing code that will happen before our async operation completes, and if we need to add more asynchronous code, we can write all of it in the same try
block, making error handling much simpler.
Hooray! If only we could magically 'promisify' our old callback method...
The Solution
I am borrowing the term 'promisify' from the Node.js method that does just that. How does it work? By simply wrapping our old function within a Promise!
const response = await new Promise((resolve, reject) => {
makeApiRequest((response, error) => {
if (error) reject(error);
resolve(response);
}, url, options);
};
We can further improve this by making our own version of Node's promisify
function:
const promisify = (oldFunction) => {
return ((...args) => (
new Promise((resolve, reject) => {
oldFunction((response, error) => {
if (error) reject(error);
resolve(response);
}, ...args);
});
));
}
const makeApiRequestWithPromise = promisify(makeApiReqeust);
Just be aware that our promisify
method is dependent on the order of arguments supplied by our makeApiRequest
method. Due to that reason, I tend to avoid using a promisify
method, and instead just inline the Promise code.
Finally, if you are promisifying a function that does not ever return an error, you can shorten this trick like so:
const example = async () => {
console.log('3..2..1');
await new Promise((resolve) => setTimeout(resolve, 1000));
console.log('Go!');
}
Top comments (0)