In Javascript, a function can be passed around just like any other value or object. Consider the following code:
const obj = { value: 2 };
const logResult = (err, result) => {
if (err) {
console.error(err);
}
console.log(result);
};
const calculate = (o, callback) => {
try {
const result = 10 / o.value;
return callback(null, result);
}
catch (err) {
return callback(err);
}
};
calculate(obj, logResult);This is typical of older style Node, known as the callback, nodeback or error-first callback style. You pass a function as the last parameter to another function, which that function calls with null in the first position, and the result in the second, or, if there was an error, the error in the first position.
Try and get your head around what is happening there. Even though it's less common to see code in this style any more in new projects (most new projects will use async/await and/or Promises), there is a lot of code out there like this, and it's great for understanding how functions can be passed around in JavaScript and Node.
When you see what's going there, consider the next snippet. Let's imagine that obj and logError were not going to be used anywhere else, and we didn't want to bother giving them names and storing them in variables:
const calculate = (o, callback) => {
try {
const result = 10 / o.value;
return callback(null, result);
}
catch (err) {
return callback(err);
}
};
calculate({ value: 2 }, (err, result) => {
if (err) {
console.error(err);
}
console.log(result);
});This does the exact same thing as the above, except now we are declaring an object { value: 2 } and an anonymous callback function inline, without storing them in variables like before. There are lots of reasons why this is incredibly useful and powerful, but you don't need to understand all of them now. Just be aware that just like you can pass a variable which holds a value/object/array/string, you can also pass a variable which holds a function, and you can also declare any of these inline, without assigning to a variable (AKA anonymously).
Why do we bother with the callback thing at all? Well imagine that line 39 took some time to complete (say it was a network request for some data or something) you could pass the callback function on to the long-running function, and the Node engine is free to do other things in the meantime. When the request finishes, the callback will fire. Node is single-threaded, so this is important so the CPU is not sitting around waiting for stuff.
That would look something like this:
const longRunningFunction = (callback) => {
setTimeout(() => {
return callback(null, 'hello');
}, 1000);
};
const calculate = (o, callback) => {
try {
return longRunningFunction(callback);
}
catch (err) {
return callback(err);
}
};
calculate({ value: 2 }, (err, result) => {
if (err) {
console.error(err);
}
console.log(result);
});I said earlier that we often don't use callbacks for long-running (async) stuff. So what does it look like now? Since Node 8 we have async/await syntax, which is a wrapper on top of Promises. The same as the above using async/await would look like this:
const returnAfterOneSecond = (o) => {
return new Promise((resolve) => {
setTimeout(() => {
return resolve(100 / o.value);
}, 1000);
});
};
const calculate = async (o) => {
try {
const result = await returnAfterOneSecond(o);
return result;
}
catch (err) {
console.log(err);
}
};
const result = await calculate({ value: 2 });
console.log(result);This is quite a bit easier to read, and write, than using callbacks. It is still important to know how they work.
So if we are not typically using callbacks for async control flow anymore, what else are they useful for? Lots of stuff. For example, lets say you wanted to take an array, and build a new array where each element was the result of a calculation on each element of the original array. This is a very common processing need. One nice way is to pass the orginal array, and the function to want to run on each element, to another function, whose job it is to run the passed-in function on each element.
This is actually how the native JavaScript Array.map function is implemented. A good excercise to understand this stuff is to implement your own map function.
Another useful case is for event handling. It's quite common to bind an anonymous function to an event, and the code within that function will then run whenever the event fires.