Callbacks

A callback is a conventional approach to providing a way to hook into asynchronous tasks. A callback is simply a function that is passed to another function and is expected to be called at some later point, possibly immediately, possibility soon, and possibly never. Consider the following requestData function:

function requestData(path, callback) {
// (Implementation of requestData)
}

As you can see, it accepts a callback as its second argument. When calling requestData, the callback will typically be anonymously passed inline, like so:

requestData('/data/123', (response) => { /* ... */ });

It is, of course, totally fine to have previously declared the callback, and doing so can aid comprehensibility as now the reader of your code will have an inkling as to when a callback might be invoked. Observe here how we're calling our onResponse callback to make clear that it is expected to be called upon the response becoming available (when it completes):

function onResponse(response) {
// Do something with the response...
}

requestData('/data/123', onResponse);

Similarly, in complex APIs with multiple asynchronous state changes, it's common to see named callbacks registered in bulk, via an object literal:

createDropdownComponent({
onOpen() {},
onSelect() {},
onClose() {},
onHover() {} // etc.
});

A callback will typically be passed arguments that indicate some important state that has been determined from the asynchronous work. For example, the Node.js readFile function invokes its callback with two arguments, a (possibly null) error and the (possibly null) data from the file itself:

fs.readFile('/path/to/file', (error, data) => {
if (error) {
// Handle the error!
} else {
// Handle the data! (No error has occurred!)
}
});

The function you pass a callback to is entirely in control of when your callback is invoked, how it is invoked, and what data is passed along with that invocation. This is why sometimes callbacks are spoken about as an inversion of control. Normally, you are in control of what functions you call, but when using callbacks, the control is inverted so that you are relying on another function or abstraction to (at some point) call your callback in the expected manner.

Callback hell is the name given to the undesirable proliferation of multiple nested callbacks within a piece of code, usually done to carry out a series of asynchronous tasks that each rely on another previous asynchronous task. Here is an example of such a situation:

requestData('/data/current-user', (userData) => {
if (userData.preferences.twitterEnabled) {
requestData(userData.twitterFeedURL, (twitterFeedData) => {
renderTwitterFeed(twitterFeedData, {
onRendered() {
logEvent('twitterFeedRender', { userId: userData.id });
}
});
});
}
});

Here, you can see we have three different callbacks, all appearing in one hierarchy of scopes. We await the response of /data/current-user, then we optionally make a request to twitterFeedURL, and then, upon the rendering of the twitter feed (renderTwitterFeed()), we finally log a "twitterFeedRender" Event. That final log depends on two previous asynchronous tasks completing and so is (seemingly unavoidably) nested quite deeply.

We can observe that this deeply nested piece of code is at the peak of a kind of horizontal pyramid of indentation. This is a common trait of callback hell, and as such, you can use the existence of these horizontal pyramids as something to watch out for. Not all deep indentations will be due to callbacks, of course, but it's usually high on the list of suspects:

To avoid the callback hell indicated by the horizontal pyramid, we should consider re-thinking and potentially re-abstracting our code. In the preceding case, logging a Twitter feed render Event, we could, for example, have a generalized function for getting and rendering Twitter feed data. This would simplify the top-level of our program:

requestData('/data/current-user', (userData) => {
if (userData.preferences.twitterEnabled) {
renderTwitterForUser(userData);
}
});

Observe how we have shortened our horizontal pyramid here. We are now free to implement renderTwitterForUser as we wish and import it as a dependency. Even though its implementation may involve its own callbacks, it is still a reduction in overall complexity for the programmer as it abstracts away half of the pyramid to a neatly separated abstraction. Most callback hell scenarios can be solved with a similar approach to re-designing and abstraction. This was a simple scenario, though. With more intertwined asynchronous tasks, it may be useful to use other mechanisms of asynchronous control flow.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.226.251.70