A callback function is a function passed into another function as an argument, which is then invoked inside the outer function to complete some kind of routine or action. There are two ways in which the callback may be called: synchronous and asynchronous. It is the most fundamental async pattern in Javascript.
For example:
A and B happen now, under the direct control of the main JS program. But C gets deferred to happen later, and under the control of another party, in this case, the ajax(..) function. In a basic sense, that sort of hand-off of control doesn’t regularly cause lots of problems for programs.
However, infrequency is not sufficient to ignore the problem or issue. In fact, it’s one of the major problems of callback-driven design. It revolves around the idea that sometimes ajax(..) or the “party” you pass your callback continuation to is not a function that you wrote, or that you directly control. Many times it’s a utility provided by some third party.
We call this “inversion of control” when you take part of your program and give over control of its execution to another third party. There’s an unspoken “contract” that exists between your code and the third-party utility — a set of things you expect to be maintained.
What is the problem with “Inversion of Control”?
There is an example for better understanding this problem.
Let’s say you’re building a travel booking website for your company. You’ve added a “book now” button that uses a third-party library’s createBooking() function. This function processes the booking and then calls your callback to send a confirmation email to the customer.
This code might look like:
Everything works perfectly during testing. People start booking travel packages on your website, and everyone is happy with your work. Suddenly, one day, you get a call from your boss regarding a major issue. A customer booked a package and received four identical confirmation emails for one booking.
You start debugging the issue. You review the part of the code that sends the confirmation email, and everything seems correct. You then investigate further and find that the createBooking() utility from the third-party library called your callback function four times, resulting in four confirmation emails being sent.
You contact the third-party library’s support team and explain the situation. They tell you they have never encountered this issue before but will prioritize it and get back to you. After a day, they call you back with their findings. They discovered that an experimental piece of code, which was not supposed to go live, had caused the createBooking() function to call the callback multiple times.
The issue was on their side, and they assured you that it has been fixed. They apologized for the trouble and confirmed that the problem would not occur again.
To prevent any unwanted issue like that after some seeking for a solution, you implement a simple if statement like the following, which the team seems happy with:
But then one of the QA engineers asks, “what happens if they never call the callback?” Oops. Neither of you had thought about that!
You start to think of all the possible things that could go wrong with them calling your callback. Here is a list of some of the problems:
Call the callback too early
Call the callback too late (or never)
Call the callback too few or too many times (like the problem in the example above)
Swallow any errors/exceptions that may happen
...
You will probably realize that you have to implement many solutions in your code for different situations, which will make the code awful and dirty because it is required in every single callback passed to a utility.
Promises
What if we could uninvert that inversion of control? What if instead of passing the continuation of our program to another party, we could expect it to return us a capability to know when its task finishes, and then our code could decide what to do next?
Promises offer a powerful way to handle asynchronous operations in JavaScript, addressing issues like callback hell and inversion of control. Unlike callbacks, Promises lets you manage asynchronous tasks without giving up control of your code.
Consider ordering a cheeseburger at a fast-food restaurant. You receive a receipt, a promise of your future cheeseburger. While you wait, you can do other things, knowing you’ll get your order eventually. Similarly, a Promise in JavaScript represents a future value, allowing your code to proceed smoothly.
Promises also handle failures gracefully, just as you might be informed if the restaurant runs out of cheeseburgers. This structure makes your code more readable and maintainable.
I’m not going to go in depth into Promises here, but by using them, you can write cleaner, more reliable asynchronous code, improving the overall quality of your JavaScript applications.
For example, for the travel booking website we described earlier, if the third-party utility returns a promise we can handle it like this:
Once a Promise is resolved (successfully completed), it stays that way forever — it becomes an immutable value at that point and can then be observed as many times as necessary. That means a resolved promise, its resolved value can be accessed or used multiple times in your code without affecting its state. This allows you to handle the result of the asynchronous operation in various parts of your application without worrying about the Promise changing or being re-evaluated.
Promises are a solution for the inversion of control issues that occur in callback-only code. Callbacks represent an inversion of control. So inverting the callback pattern is actually an inversion of inversion, or an uninversion of control. Restoring control back to the calling code where we wanted it to be in the first place.
References
You Don’t Know JS: Async & Performance by Kyle Simpson
Top comments (0)