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 asynchronous 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 forresolutionFunc
andrejectionFunc
; these are "tethered" to thePromise
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 theresolutionFunc
or therejectionFunc
, respectively.- In other words, the code within the
executor
communicates via the side effect caused byresolutionFunc
orrejectionFunc
. The side effect is that thePromise
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.