DEV Community

SebasQuiroga
SebasQuiroga

Posted on

Async JS Patterns using Callback

JavaScript, one of the most used computing programming languages in today's world [reference], very criticized for its simplicity compared with Java or C++, but unquestionably, this is precisely one of the characteristics that makes it as powerful and globally spread.

Along these series of managing JS concurrency, we will discuss about some of the weapons we should probably all have in our personal arsenal when developing in JS; the main objective of these series is precisely train you in the combat field using those weaponry.

It is important to highlight that even when there are new fancy ways of solving concurrency problems, each mechanism has it own advantages/disadvantage and use-cases.

In this first episode we will talk about callbacks, a pretty nice initial way of solving concurrent tasks.

JS engine behaves pretty similar to the electricity, it is always looking for the path with less resistance, it will jump anything that might seem slow (setTimeouts, API calls, rendering etc) and go crazy executing the fastest instructions unless we use mechanisms for dealing with it 😉 (such as callback, thunks, promises, generator functions, async functions).
Jumping

Callbacks 🤔

It all comes from functional programming where some brilliant guys back in 1930 introduced what is know known as lambda calculus, this topic is deep enough to another article but let's continue.

Let's go over the next function, it basically expects two arguments and prints out the sum of those, pretty simple, isn't it?

function foo(a, b) {
    console.info(a + b); // 3
}
foo(1,2);
Enter fullscreen mode Exit fullscreen mode

But what about sending the reference of a function instead of some variables? - what would happen?

// A function for sum
function sum(a, b){
    return a + b;
}

// A function that orchestrates
function foo(callback, a , b){
    return callback(a,b) // this line will call our sum() 
}

const resp = foo(sum, 1, 2) // we pass the reference of the function sum() and two numbers
console.info(resp); // 3
Enter fullscreen mode Exit fullscreen mode

Oww we just chained functions in a pretty awesome way, so we could also add a new function subtract so that just changing the reference to the new function, we can either sum or subtract.

function sum(a, b){
    return a + b;        
}

// New Subtract function
function sub(a, b){
    return a - b;      
}

function foo(callback, a , b){
    return callback(a,b) // this line will call either sum() or sub()
}

const respSum = foo(sum, 1, 2);
const respSub = foo(sub, 1, 2);  // We can now subtract too!

console.info(` Sum:  ${respSum}` ); // 3
console.info(` Sub:  ${respSub}`); // -1
Enter fullscreen mode Exit fullscreen mode

The Callback pattern is very used but it is even more powerful when regarding asynchronicity; hopefully our apps need to communicate with other APIs for fetching, saving, updating data, this brings some new players in the game; we need to play with the mess produced by the JS engine when trying to execute as many instructions as possible without worrying about
sequential/blocking execution and human reasonability; dear reader, this is what I call asynchronicity heartbreak.

You can copy, paste and execute the next code and you will see how the JS engine prints ‘Second Line’ first and then ‘First Line’; in the opposite order as we really want.

function fetchFoo() {
    setTimeout(() => {
        console.info('First Line')
    }, 2000)
    console.info('Second Line')
}
fetchFoo()
Enter fullscreen mode Exit fullscreen mode

What is really happening is when the JS engine executes fetchFoo() it will first try to read and solve the first line but as the setTimeout is waiting 2 seconds it jumps to the next instruction and prints 'Second Line' first.

Theoretically speaking one way of solving this puzzle is manually passing asynchronous tasks as they were synchronous, so that the JS engine doesn't have any other branch of code to prioritize and as soon as it finishes we pass new tasks and so on. Let’s revise the next code.

// Chaining callbacks as a way of forcing sequential execution

function fooBar() {
    cb1()
    function cb1() {

        setTimeout(function () {
            console.info('First Line')
            cb2()
        }, 1000)

        function cb2() {
            console.info('Second Line')
        }
    }
}

fooBar()
Enter fullscreen mode Exit fullscreen mode

We are basically forcing a link of consecutive instructions so that the JS engine doesn't even have to think an easier path to follow, it doesn’t have any more option that waiting 1 second and printing the ‘First Line’ and then the ‘Second Line’ as we originally wanted.

The next code is a cleaner and more beautiful approach using the same principle.

function fooBar(cb1) {
    setTimeout(function () {
                // The function cb1() expects another callback - the cb2()
        cb1(function cb2 () {
            console.info('Second Line')
        })
    }, 1000)
}

// The function cb1() will eventually read and execute another callback that is not still defined.
// We will eventually define it with the next sequential instructions.
fooBar(function cb1 (cb2) {
    console.info('First Line')
    cb2()
})
Enter fullscreen mode Exit fullscreen mode

We are basically saying: hey JS engine, you know what? - I will pass to you another function in a moment, I can not pass it to you right now because you will try to execute it in a way I don't want, so don't worry, just go ahead with your business and I call you later. When the timer reaches the 1 second and the ‘First Line’ is printed, we pass to it a brand new code (with the string ‘Second Line’) to execute, this is basically known as lazy execution.

Callbacks are a mechanism for lazy execution.

As almost everything in life, there are some considerations that need to be carefully taken. When using callbacks we will need to agree with the inversion of control, giving the control of our program to somebody else. we have probably all have to invoke an external API using this mechanism and living the horror of expecting that API doesn't fail, simply because there is no way that API notifies us, so that, we can code an emergency exit, we basically loose the control of our program, Kyle Simpson said in a talk regarding callbacks:

"There's part of my program that I'm in control of executing. And then, there's another portion of my code that I'm not in control of executing."

There is another problem ... trust, let's imagine that somehow the setTimeout eventually start failing, sometimes instead of timing 1 second it continues to 100 seconds! - that is a really pain!! - our program will take much longer that what we initially expected, or even worse, image that it never stops !! - Callbacks lack of some sort of mechanism that notifies us when something goes wrong so that we can code an emergency exit and protecting our computer power.

A part of that. there is still another problem with callback, they don’t match our sequential blocking way of thinking, Kyle Simpson says in one of his talks: (paraphrasing):

Bugs arise when code doesn’t work as our brain does.

Our natural way of solving concurrency problems differ from the way callbacks work, we probably would need another pattern for solving asynchronous tasks that doesn’t force us neither to lose the control (inversion of control) of our programs nor the lack of trust when third party APIs are involved and by the way, a more *reason*able approach.

The real problem with callbacks is deeper than just the callback hell

In the next post we will talk about Thunks, a slightly different callback pattern.

References

  • [Book] You Don’t Know JS Async & Performance
  • MDN

Top comments (0)