Last week, we learned how callback functions let us handle asynchronous operations in vanilla JS. Since ES2015 (ES6), vanilla JS has supported promises. A Promise is an object representing the eventual completion or failure of an asynchronous operation, as the MDN Web Docs phrase it. Promises are a way of writing asynchronous code which avoids the pyramid of doom associated with nested callback functions, affectionately known as callback hell. Today, we'll learn how to convert a function which accepts a callback function into a function which returns a Promise.

In case you want to test the code in this post, I've made a demo on StackBlitz.

Let's revisit our getData() function from last week. It accepts a callback() function. The callback() function accepts two arguments: error and data. If there's an "error", we pass it into our callback() function as the first argument (error); otherwise, we pass in the "API data" as the second argument (data):

function getData(callback) {
function fiftyFifty() {
return Math.round(Math.random());
}

setTimeout(function () {
// If there's an "error", pass it in as the first argument
if (!fiftyFifty()) {
callback(new Error("500 Interval Server Error"), null);
return;
}

// Otherwise, pass in the "API data" as the second argument
callback(null, [
{ id: 1, name: "SpongeBob" },
{ id: 2, name: "Patrick" },
{ id: 3, name: "Squidward" },
]);
}, 3000);
}

Here's how we can rewrite this so that it returns a Promise:

function getData() {
function fiftyFifty() {
return Math.round(Math.random());
}

function executor(resolutionFunc, rejectionFunc) {
setTimeout(function () {
// If there's an "error", reject the Promise with it
if (!fiftyFifty()) {
rejectionFunc(new Error("500 Interval Server Error"));
return;
}

// Otherwise, resolve the Promise with the "API data"
resolutionFunc([
{ id: 1, name: "SpongeBob" },
{ id: 2, name: "Patrick" },
{ id: 3, name: "Squidward" },
]);
}, 3000);
}

return new Promise(executor);
}

Notice that this code still uses callback functions! They're essential to asychronous JavaScript code; all we're doing is wrapping everything in a Promise so that it's easier to work with later. We pass the executor() function into the Promise() constructor as a callback function. The executor() function itself accepts two callback functions: resolutionFunc() and rejectionFunc().

I'll defer to the MDN Web Docs to explain what's happening here:

  • At the time when the constructor generates the new Promise object, it also generates a corresponding pair of functions for resolutionFunc and rejectionFunc; these are "tethered" to the Promise object.
  • The code within the executor has the opportunity to perform some operation and then reflect the operation's outcome … as either "resolved" or "rejected", by terminating with an invocation of either the resolutionFunc or the rejectionFunc, respectively.
  • In other words, the code within the executor communicates via the side effect caused by resolutionFunc or rejectionFunc. The side effect is that the Promise object either becomes "resolved", or "rejected".

Here's what all of this means. Before, we had to pass a callback function into our getData() function:

getData(function (error, data) {
// If there's an error, log it
if (error) {
console.error(error);
return;
}

// Otherwise, do something with the data
console.log(data);
});

Now that we've wrapped our getData() function in a Promise, we can invoke it without any arguments. Instead, we chain a call to the .then() method, and pass two callback functions into that:

// Call console.log() on resolve; call console.error() on reject
getData().then(console.log, console.error);

Again, notice that we're still using callback functions. We're passing in the console.log() method as the callback function for a successful (resolved) outcome, and the console.error() method as the callback function for a failed (rejected) outcome. The difference is that promises let us write this with a flat structure, which helps us to avoid callback hell.

In this post, we've used the Promise() constructor to convert a function which accepts a callback function into a function which returns a Promise. This is a useful skill, but most of the time, you'll be working with code which is already promise-based, such as the Fetch API.

Next week, we'll explore the .then(), .catch(), and .finally() methods in more depth, which are the methods you use to consume promises.