Bits and Pieces

Insightful articles, step-by-step tutorials, and the latest news on full-stack composable software…

Follow publication

The Good and Bad of JavaScript Callbacks

Callbacks are functions that are called back later. If you think I didn’t explain anything in the previous sentence, I did emphasize one important identity of a callback — a function. The callback function — including its references — is passed as an argument to other functions. The functions that receive the callback function as a parameter are the ones responsible to call back the callback function. Callbacks are great because they open up a lot of programming possibilities.

Synchronous Callbacks

Many people, especially those who are old school, had their first encounter with callbacks when they learned that different comparison functions can be supplied to the same sorting algorithm. For example, when using the Array.prototype.sort() method to sort an array of integers, an optional parameter is the comparison function compareFn.

let arr1 = [22, 25, 55, 66, 23, 15, 1, 12]
arr1.sort() // arr1 is sorted in ascending order
// arr1 becomes [1, 12, 15, 22, 23, 25, 55, 66]
let arr2 = […arr1] // deep copy arr1 to arr2
arr2.sort((e1, e2)=> e2-e1) // arr2 is sorted in descending order
// arr2 becomes [66, 55, 25, 23, 22, 15, 12, 1 ]
// (e1, e2)=> e2-e1 is the comparison function

This type of callbacks are available in most programming languages, achieving the effect of polymorphic algorithms.

The above illustrates how synchronous callbacks work. Synchronous callbacks are executed inside the high-order functions that use them. When the high-order function is done executing, execution of its callback arguments also completes. Since the high-order function has to wait for the completion of synchronous callback execution, synchronous callbacks are also referred to as blocking callbacks — the execution of callbacks block the execution of their caller functions.

Some additional examples of synchronous callbacks in JavaScript are methods for iterating arrays: forEach, map, filter, reduce, some, every , etc.

let arr = [1,2,3,4,5]
let arrDoubled = arr.map(e=>e+e)
console.log(arrDoubled) // output [ 2, 4, 6, 8, 10 ]

Asynchronous Callbacks

If synchronous callbacks are ways to achieve greater programming flexibility, then asynchronous callbacks are ways to achieve greater performance and user experience. The power of asynchronous callbacks lie in JavaScript’s unique runtime model. JavaScript is a single threaded language, meaning that the execution engine of JavaScript only has one call stack.

Then how does asynchronous execution achieved?

The magic is in the API handlers of the JavaScript runtime environment. In the case of web browsers, the APIs are the web APIs; in the case of Node.js, the APIs are the I/O APIs. Tasks of executing asynchronous callbacks are put into the callback queue.

After existing code in the call stack runs to its completion, event loop — a process that is part of the JavaScript engine — brings the callbacks in the callback queue to execution.

Once a callback is run by the execution engine, it is again run to its completion (until the call stack is empty) before the next callback starts to run. This run-to-completion for code on the call stack continues until all the callbacks in the queue are executed.

The asynchronous aspect comes from the fact that a callback is not executed immediately in the high order function, but is put in the callback queue to wait for its turn to be run on the call stack.

The high order function is the one to dispatch the callback task, but not the one to run it.

The most prevalent example of using an asynchronous callback is using the setTimeOut method.

console.log("before setTimeout")
setTimeout(
()=>{ console.log("result is here after 2 seconds") },
2000
)
console.log("after setTimeout")

This method is in the global object window in the case of browsers and global in the case of Node.js.

The first parameter is the callback function, the second parameter is the time to wait in milliseconds. setTimeout does not need to wait for the callback to complete before it returns.

Here is how the output looks like:

before setTimeout
after setTimeout
result is here after 2 seconds

The benefit of the asynchronous callback mechanism is that all the synchronous code is not blocked by asynchronous events. Asynchronous events (e.g. AJAX requests to remote servers) potentially can take some time to run. Web applications can run more smoothly and responsively with asynchronous callbacks.

Callback Hell

You must have heard the term “callback hell” before. It sounds scary, but in reality is just aesthetically not that appealing and functionally prone to error and hard to debug and maintain.

Imagine an e-commerce web application. One of the functions provided by the website is to obtain detailed history of order information for the given customer. There are several steps needed to obtain the required information:

  1. Get the user information.
  2. Get the list of order IDs for the user
  3. Get the list of products for each order
  4. Compile the information obtained in the previous steps into a report

Translate into code — callback hell style:

Note that setTimeout is used to simulate querying databases to get the corresponding data. The indented callbacks here may not seem to be too bad, but in practice the level of callbacks can reach to a much bigger number. With the presence of other control logic and error handling, the code for sure will go out of the screen.

There is Always Promise

To address the problem described in the previous section, ES6 introduced Promise as JavaScript’s first level constructor, just like String, Date, Array, etc.

Here is how to use promises to resolve the callback hell issue:

Note how the asynchronous requests to the databases (simulated with setTimeout) are wrapped inside a Promise constructor.

Also note that for getUserInfo, getOrderInfo, getProductInfo functions, callbacks are no longer needed.

In the end, each step returns a Promise object and the results are chained through the then methods.

All the steps become a vertical chain of promises versus a horizontal stretch of callbacks.

Promise Comes to the Rescue of the Callback Hell

Stay tuned for my next article explaining the details of Promise and its inner workings.

Build composable web applications

Don’t build web monoliths. Use Bit to create and compose decoupled software components — in your favorite frameworks like React or Node. Build scalable and modular applications with a powerful and enjoyable dev experience.

Bring your team to Bit Cloud to host and collaborate on components together, and speed up, scale, and standardize development as a team. Try composable frontends with a Design System or Micro Frontends, or explore the composable backend with serverside components. Give it a try →

Learn More

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

Published in Bits and Pieces

Insightful articles, step-by-step tutorials, and the latest news on full-stack composable software development

No responses yet