Last week, we looked at understanding callback functions in vanilla JS. We learned that a callback function is just a function that you pass into another function as an argument. But we haven't yet learned how callback functions let us handle asynchronous operations. Let's change that!

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

The problem

Imagine we have a function called getData(). We want it to get some data from an API and return it. It doesn't work!

For the sake of this demo, we're using:

function getData() {
// 1. Declare a variable to reference the data
let data;

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

// 3. Try to assign a value three seconds too late
setTimeout(function () {
if (!fiftyFifty()) {
data = new Error("500 Interval Server Error");
} else {
data = [
{ id: 1, name: "SpongeBob" },
{ id: 2, name: "Patrick" },
{ id: 3, name: "Squidward" },
];
}
}, 3000);

return data; // 2. Return the undefined value
}

const data = getData(); // undefined

We declare data without assigning it a value, meaning its value is undefined. Then we try to assign it the "API data" after three seconds, before returning it. The trouble is that the return statement runs first; it doesn't wait for the "API call" to finish.

The solution

We can fix this by modifying our getData() function so that it accepts a callback function as an argument. The callback function itself will accept error and data arguments:

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);
}

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);
});

We don't try to assign a value after three seconds and then return it. Instead, we invoke our callback function after three seconds, passing in an Error or our "API data", whichever is applicable. This means that when we invoke getData(), it then invokes our callback function after three seconds. Our callback function receives the final values as arguments!

Promises

Until promises and async/await were introduced, callback functions were the only way to handle asynchronous operations in JavaScript. It's still important to understand them, because the newer options are really just wrappers around callback functions, designed to make our code easier to read.

Next week, we'll learn how to use the Promise() constructor to modify our getData() function so that it returns a Promise.